/* This file is part of darktable, Copyright (C) 2009-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable 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 General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "bauhaus/bauhaus.h" #include "common/imagebuf.h" #include "common/iop_profile.h" #include "common/colormatrices.c" #include "common/colorspaces.h" #include "common/hdr-transfer.h" #include "common/colorspaces_inline_conversions.h" #include "common/image_cache.h" #include "common/opencl.h" #include "common/utility.h" #include "control/control.h" #include "develop/develop.h" #include "gui/gtk.h" #include "gui/accelerators.h" #include "imageio/imageio_jpeg.h" #include "imageio/imageio_png.h" #ifdef HAVE_LIBJXL #include "imageio/imageio_jpegxl.h" #endif #include "imageio/imageio_tiff.h" #ifdef HAVE_LIBAVIF #include "imageio/imageio_avif.h" #endif #ifdef HAVE_LIBHEIF #include "imageio/imageio_heif.h" #endif #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "iop/iop_api.h" #include #include #include #include #include #include // max iccprofile file name length // must be in synch with dt_colorspaces_color_profile_t #define DT_IOP_COLOR_ICC_LEN 512 #define LUT_SAMPLES 0x10000 // grid of the LittleCMS transform baked for the GPU, see _bake_clut() #define CLUT_LEVEL 65 DT_MODULE_INTROSPECTION(8, dt_iop_colorin_params_t) static void update_profile_list(dt_iop_module_t *self); typedef enum dt_iop_color_normalize_t { DT_NORMALIZE_OFF, //$DESCRIPTION: "off" DT_NORMALIZE_SRGB, //$DESCRIPTION: "sRGB" DT_NORMALIZE_ADOBE_RGB, //$DESCRIPTION: "Adobe RGB (compatible)" DT_NORMALIZE_LINEAR_REC709_RGB, //$DESCRIPTION: "linear Rec709 RGB" DT_NORMALIZE_LINEAR_REC2020_RGB //$DESCRIPTION: "linear Rec2020 RGB" } dt_iop_color_normalize_t; typedef struct dt_iop_colorin_params_t { dt_colorspaces_color_profile_type_t type; // $DEFAULT: DT_COLORSPACE_ENHANCED_MATRIX char filename[DT_IOP_COLOR_ICC_LEN]; dt_iop_color_intent_t intent; // $DEFAULT: DT_INTENT_PERCEPTUAL dt_iop_color_normalize_t normalize; // $DEFAULT: DT_NORMALIZE_OFF $DESCRIPTION: "gamut clipping" gboolean blue_mapping; // working color profile dt_colorspaces_color_profile_type_t type_work; // $DEFAULT: DT_COLORSPACE_LIN_REC2020 char filename_work[DT_IOP_COLOR_ICC_LEN]; gboolean output_rgb; // $DEFAULT: TRUE } dt_iop_colorin_params_t; typedef struct dt_iop_colorin_gui_data_t { GtkWidget *profile_combobox, *clipping_combobox, *work_combobox; GList *image_profiles; int n_image_profiles; } dt_iop_colorin_gui_data_t; typedef struct dt_iop_colorin_global_data_t { int kernel_colorin_unbound; int kernel_colorin_clipping; int kernel_colorin_rgb; int kernel_colorin_clut; } dt_iop_colorin_global_data_t; typedef struct dt_iop_colorin_data_t { gboolean clear_input; gboolean output_rgb; const dt_iop_order_iccprofile_info_t *work_profile; dt_colormatrix_t rgb_matrix; dt_colormatrix_t rgb_lmatrix; cmsHPROFILE input; cmsHPROFILE nrgb; cmsHTRANSFORM *xform_cam_out; cmsHTRANSFORM *xform_cam_nrgb; cmsHTRANSFORM *xform_nrgb_out; float *clut; // xform_cam_out baked into a CLUT_LEVEL^3 LUT for the GPU, built on first use float lut[3][LUT_SAMPLES]; dt_colormatrix_t cmatrix; dt_colormatrix_t nmatrix; dt_colormatrix_t lmatrix; float unbounded_coeffs[3][3]; // approximation for extrapolation of shaper curves gboolean blue_mapping; gboolean nonlinearlut; int hdr_transfer; dt_colorspaces_color_profile_type_t type; dt_colorspaces_color_profile_type_t type_work; char filename[DT_IOP_COLOR_ICC_LEN]; char filename_work[DT_IOP_COLOR_ICC_LEN]; float *hsm; int hue_div; int sat_div; int val_div; int hsm_encode; } dt_iop_colorin_data_t; const char *name() { return _("input color profile"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("convert any RGB input to pipeline reference RGB\n" "using color profiles to remap RGB values"), _("mandatory"), _("linear or non-linear, RGB, scene-referred"), _("defined by profile"), _("linear, RGB, scene-referred")); } int default_group() { return IOP_GROUP_COLOR | IOP_GROUP_TECHNICAL; } int flags() { return IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_ONE_INSTANCE; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } dt_iop_colorspace_type_t input_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { if(piece) { const dt_iop_colorin_data_t *const d = piece->data; if(d->type == DT_COLORSPACE_LAB) return IOP_CS_LAB; } return IOP_CS_RGB; } dt_iop_colorspace_type_t output_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { const dt_iop_colorin_data_t *d = piece ? piece->data : NULL; return (!d || d->output_rgb) ? IOP_CS_RGB : IOP_CS_LAB; } static void _resolve_work_profile(dt_colorspaces_color_profile_type_t *work_type, char *work_filename) { for(GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = l->data; if(prof->work_pos > -1 && *work_type == prof->type && (prof->type != DT_COLORSPACE_FILE || dt_colorspaces_is_profile_equal(prof->filename, work_filename))) return; } dt_print(DT_DEBUG_ALWAYS, "[colorin] profile `%s' not suitable for work profile." " it has been replaced by linear Rec2020 RGB!", dt_colorspaces_get_name(*work_type, work_filename)); *work_type = DT_COLORSPACE_LIN_REC2020; work_filename[0] = '\0'; } // Start of dng image support for ProfileHueSatMapDims and ProfileHueSatMapData // the used set is chosen via the illuminant in exif.cc static inline void _lookup_hsm(const dt_aligned_pixel_t hsv, dt_aligned_pixel_t correction, const int hue_div, const int sat_div, const int val_div, const float *hsm) { const float h = (hsv[0] - floorf(hsv[0])) * (float)hue_div; const float s = CLIP(hsv[1]) * (float)(sat_div-1); const float v = CLIP(hsv[2]) * (float)(val_div-1); const int hi[2] = { MIN((int)h, hue_div-1), (MIN((int)h, hue_div-1)+1) % hue_div }; const int si[2] = { MIN((int)s, sat_div-1), MIN((int)s+1, sat_div-1) }; const int vi[2] = { MIN((int)v, val_div-1), MIN((int)v+1, val_div-1) }; const float hf = CLIP(h - hi[0]); const float sf = CLIP(s - si[0]); const float vf = CLIP(v - vi[0]); correction[0] = correction[1] = correction[2] = 0.0f; for(int z = 0; z < 2; z++) for(int y = 0; y < 2; y++) for(int x = 0; x < 2; x++) { // DNG stores saturation first, then hue, then value, with three floats per cell const size_t index = 3 * (((size_t)vi[z] * hue_div + hi[y]) * sat_div + si[x]); const float weight = (z ? vf : 1.0f - vf) * (y ? hf : 1.0f - hf) * (x ? sf : 1.0f - sf); for(int c = 0; c < 3; c++) correction[c] += weight * hsm[index + c]; } } // the DNG hue/saturation tables are specified for ProPhoto primaries // and are scaled to levels without any exposure or RGB coeffs as found // after rawprepare. static inline float _srgb_to_linear(const float x) { return x <= 0.04045f ? x / 12.92f : powf((x + 0.055f) / 1.055f, 2.4f); } static inline float _linear_to_srgb(const float x) { return x <= 0.0031308f ? 12.92f * x : 1.055f * powf(x, 1.0f / 2.4f) - 0.055f; } // legacy buffers contain Lab; RGB buffers are still linear here so the // working profile is encoded only after applying the look static void _process_dng_look(float *data, const size_t width, const size_t height, const dt_iop_colorin_data_t *d, const float exposure) { DT_OMP_FOR(collapse(2)) for(size_t row = 0; row < height; row++) { for(size_t col = 0; col < width; col++) { const size_t k = (size_t)4 * (width * row + col); dt_aligned_pixel_t prophoto_rgb = { 0.0f }; dt_aligned_pixel_t hsv; dt_aligned_pixel_t correction; dt_aligned_pixel_t out; if(d->output_rgb) { dt_aligned_pixel_t xyz; dt_apply_transposed_color_matrix(&data[k], d->work_profile->matrix_in_transposed, xyz); dt_XYZ_to_prophotorgb(xyz, prophoto_rgb); } else dt_Lab_to_prophotorgb(&data[k], prophoto_rgb); dt_vector_div1(prophoto_rgb, prophoto_rgb, exposure); dt_RGB_2_HSV(prophoto_rgb, hsv); float v_encoded = hsv[2]; if(d->hsm_encode == 1) // sRGB v_encoded = _linear_to_srgb(CLIP(v_encoded)); dt_aligned_pixel_t hsv_for_lookup = { hsv[0], hsv[1], v_encoded, 0.0f}; _lookup_hsm(hsv_for_lookup, correction, d->hue_div, d->sat_div, d->val_div, d->hsm); hsv[0] += correction[0] / 360.0f; hsv[0] -= floorf(hsv[0]); // stay within the 360° hue circle hsv[1] = CLIP(hsv[1] * correction[1]); float v_corrected = v_encoded * correction[2]; if(d->hsm_encode == 1) // sRGB v_corrected = _srgb_to_linear(v_corrected); hsv[2] = CLIP(v_corrected); dt_HSV_2_RGB(hsv, prophoto_rgb); dt_vector_mul1(prophoto_rgb, prophoto_rgb, exposure); if(d->output_rgb) { dt_aligned_pixel_t xyz, linear; dt_prophotorgb_to_XYZ(prophoto_rgb, xyz); dt_apply_transposed_color_matrix(xyz, d->work_profile->matrix_out_transposed, linear); linear[3] = data[k+3]; dt_ioppr_encode_rgb(linear, out, d->work_profile); } else dt_prophotorgb_to_Lab(prophoto_rgb, out); out[3] = data[k+3]; // copy alpha copy_pixel(&data[k], out); } } } static void _commit_hsm(dt_iop_colorin_data_t *d, const dt_imgid_t imgid) { // pipe->image borrows its table; keep the cache locked through validation and copying const dt_image_t *img = dt_image_cache_get(imgid, 'r'); // safety check before committing if(img && img->profile_hsm_data && img->profile_hsm_hue_div >= 1 && img->profile_hsm_sat_div >= 2 && img->profile_hsm_val_div >= 1 && img->profile_hsm_hue_div <= (G_MAXINT / 3 / img->profile_hsm_sat_div / img->profile_hsm_val_div)) { const int count = 3 * img->profile_hsm_hue_div * img->profile_hsm_sat_div * img->profile_hsm_val_div; if((size_t)count > img->profile_hsm_data_size / sizeof(float)) { dt_image_cache_read_release(img); return; } gboolean valid = TRUE; for(int i = 0; i < count; i++) { if(!isfinite(img->profile_hsm_data[i]) || (i % 3 != 0 && img->profile_hsm_data[i] < 0.0f)) { valid = FALSE; break; } } if(valid) { d->hsm = g_malloc_n(count, sizeof(float)); memcpy(d->hsm, img->profile_hsm_data, (size_t)count * sizeof(float)); d->hue_div = img->profile_hsm_hue_div; d->sat_div = img->profile_hsm_sat_div; d->val_div = img->profile_hsm_val_div; d->hsm_encode = img->profile_hsm_encoding; } } dt_image_cache_read_release(img); } int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void **new_params, int32_t *new_params_size, int *new_version) { typedef struct dt_iop_colorin_params_v7_t { dt_colorspaces_color_profile_type_t type; char filename[DT_IOP_COLOR_ICC_LEN]; dt_iop_color_intent_t intent; dt_iop_color_normalize_t normalize; gboolean blue_mapping; // working color profile dt_colorspaces_color_profile_type_t type_work; char filename_work[DT_IOP_COLOR_ICC_LEN]; } dt_iop_colorin_params_v7_t; #define DT_IOP_COLOR_ICC_LEN_V5 100 if(old_version == 1) { typedef struct dt_iop_colorin_params_v1_t { char iccprofile[DT_IOP_COLOR_ICC_LEN_V5]; dt_iop_color_intent_t intent; } dt_iop_colorin_params_v1_t; const dt_iop_colorin_params_v1_t *old = (dt_iop_colorin_params_v1_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memset(new, 0, sizeof(*new)); if(!strcmp(old->iccprofile, "eprofile")) new->type = DT_COLORSPACE_EMBEDDED_ICC; else if(!strcmp(old->iccprofile, "ematrix")) new->type = DT_COLORSPACE_EMBEDDED_MATRIX; else if(!strcmp(old->iccprofile, "cmatrix")) new->type = DT_COLORSPACE_STANDARD_MATRIX; else if(!strcmp(old->iccprofile, "darktable")) new->type = DT_COLORSPACE_ENHANCED_MATRIX; else if(!strcmp(old->iccprofile, "vendor")) new->type = DT_COLORSPACE_VENDOR_MATRIX; else if(!strcmp(old->iccprofile, "alternate")) new->type = DT_COLORSPACE_ALTERNATE_MATRIX; else if(!strcmp(old->iccprofile, "sRGB")) new->type = DT_COLORSPACE_SRGB; else if(!strcmp(old->iccprofile, "adobergb")) new->type = DT_COLORSPACE_ADOBERGB; else if(!strcmp(old->iccprofile, "linear_rec709_rgb") || !strcmp(old->iccprofile, "linear_rgb")) new->type = DT_COLORSPACE_LIN_REC709; else if(!strcmp(old->iccprofile, "linear_rec2020_rgb")) new->type = DT_COLORSPACE_LIN_REC2020; else if(!strcmp(old->iccprofile, "infrared")) new->type = DT_COLORSPACE_INFRARED; else if(!strcmp(old->iccprofile, "XYZ")) new->type = DT_COLORSPACE_XYZ; else if(!strcmp(old->iccprofile, "Lab")) new->type = DT_COLORSPACE_LAB; else { new->type = DT_COLORSPACE_FILE; dt_strlcpy_fixed_to_fixed(new->filename, sizeof(new->filename), old->iccprofile, sizeof(old->iccprofile)); } new->intent = old->intent; new->normalize = 0; new->blue_mapping = TRUE; new->type_work = DT_COLORSPACE_LIN_REC709; new->filename_work[0] = '\0'; *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 2) { typedef struct dt_iop_colorin_params_v2_t { char iccprofile[DT_IOP_COLOR_ICC_LEN_V5]; dt_iop_color_intent_t intent; int normalize; } dt_iop_colorin_params_v2_t; const dt_iop_colorin_params_v2_t *old = (dt_iop_colorin_params_v2_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memset(new, 0, sizeof(*new)); if(!strcmp(old->iccprofile, "eprofile")) new->type = DT_COLORSPACE_EMBEDDED_ICC; else if(!strcmp(old->iccprofile, "ematrix")) new->type = DT_COLORSPACE_EMBEDDED_MATRIX; else if(!strcmp(old->iccprofile, "cmatrix")) new->type = DT_COLORSPACE_STANDARD_MATRIX; else if(!strcmp(old->iccprofile, "darktable")) new->type = DT_COLORSPACE_ENHANCED_MATRIX; else if(!strcmp(old->iccprofile, "vendor")) new->type = DT_COLORSPACE_VENDOR_MATRIX; else if(!strcmp(old->iccprofile, "alternate")) new->type = DT_COLORSPACE_ALTERNATE_MATRIX; else if(!strcmp(old->iccprofile, "sRGB")) new->type = DT_COLORSPACE_SRGB; else if(!strcmp(old->iccprofile, "adobergb")) new->type = DT_COLORSPACE_ADOBERGB; else if(!strcmp(old->iccprofile, "linear_rec709_rgb") || !strcmp(old->iccprofile, "linear_rgb")) new->type = DT_COLORSPACE_LIN_REC709; else if(!strcmp(old->iccprofile, "linear_rec2020_rgb")) new->type = DT_COLORSPACE_LIN_REC2020; else if(!strcmp(old->iccprofile, "infrared")) new->type = DT_COLORSPACE_INFRARED; else if(!strcmp(old->iccprofile, "XYZ")) new->type = DT_COLORSPACE_XYZ; else if(!strcmp(old->iccprofile, "Lab")) new->type = DT_COLORSPACE_LAB; else { new->type = DT_COLORSPACE_FILE; dt_strlcpy_fixed_to_fixed(new->filename, sizeof(new->filename), old->iccprofile, sizeof(old->iccprofile)); } new->intent = old->intent; new->normalize = old->normalize; new->blue_mapping = TRUE; new->type_work = DT_COLORSPACE_LIN_REC709; new->filename_work[0] = '\0'; *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 3) { typedef struct dt_iop_colorin_params_v3_t { char iccprofile[DT_IOP_COLOR_ICC_LEN_V5]; dt_iop_color_intent_t intent; int normalize; gboolean blue_mapping; } dt_iop_colorin_params_v3_t; const dt_iop_colorin_params_v3_t *old = (dt_iop_colorin_params_v3_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memset(new, 0, sizeof(*new)); if(!strcmp(old->iccprofile, "eprofile")) new->type = DT_COLORSPACE_EMBEDDED_ICC; else if(!strcmp(old->iccprofile, "ematrix")) new->type = DT_COLORSPACE_EMBEDDED_MATRIX; else if(!strcmp(old->iccprofile, "cmatrix")) new->type = DT_COLORSPACE_STANDARD_MATRIX; else if(!strcmp(old->iccprofile, "darktable")) new->type = DT_COLORSPACE_ENHANCED_MATRIX; else if(!strcmp(old->iccprofile, "vendor")) new->type = DT_COLORSPACE_VENDOR_MATRIX; else if(!strcmp(old->iccprofile, "alternate")) new->type = DT_COLORSPACE_ALTERNATE_MATRIX; else if(!strcmp(old->iccprofile, "sRGB")) new->type = DT_COLORSPACE_SRGB; else if(!strcmp(old->iccprofile, "adobergb")) new->type = DT_COLORSPACE_ADOBERGB; else if(!strcmp(old->iccprofile, "linear_rec709_rgb") || !strcmp(old->iccprofile, "linear_rgb")) new->type = DT_COLORSPACE_LIN_REC709; else if(!strcmp(old->iccprofile, "linear_rec2020_rgb")) new->type = DT_COLORSPACE_LIN_REC2020; else if(!strcmp(old->iccprofile, "infrared")) new->type = DT_COLORSPACE_INFRARED; else if(!strcmp(old->iccprofile, "XYZ")) new->type = DT_COLORSPACE_XYZ; else if(!strcmp(old->iccprofile, "Lab")) new->type = DT_COLORSPACE_LAB; else { new->type = DT_COLORSPACE_FILE; dt_strlcpy_fixed_to_fixed(new->filename, sizeof(new->filename), old->iccprofile, sizeof(old->iccprofile)); } new->intent = old->intent; new->normalize = old->normalize; new->blue_mapping = old->blue_mapping; new->type_work = DT_COLORSPACE_LIN_REC709; new->filename_work[0] = '\0'; *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 4) { typedef struct dt_iop_colorin_params_v4_t { dt_colorspaces_color_profile_type_t type; char filename[DT_IOP_COLOR_ICC_LEN_V5]; dt_iop_color_intent_t intent; int normalize; gboolean blue_mapping; } dt_iop_colorin_params_v4_t; const dt_iop_colorin_params_v4_t *old = (dt_iop_colorin_params_v4_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memset(new, 0, sizeof(*new)); new->type = old->type; dt_strlcpy_fixed_to_fixed(new->filename, sizeof(new->filename), old->filename, sizeof(old->filename)); new->intent = old->intent; new->normalize = old->normalize; new->blue_mapping = old->blue_mapping; new->type_work = DT_COLORSPACE_LIN_REC709; new->filename_work[0] = '\0'; *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 5) { typedef struct dt_iop_colorin_params_v5_t { dt_colorspaces_color_profile_type_t type; char filename[DT_IOP_COLOR_ICC_LEN_V5]; dt_iop_color_intent_t intent; int normalize; gboolean blue_mapping; // working color profile dt_colorspaces_color_profile_type_t type_work; char filename_work[DT_IOP_COLOR_ICC_LEN_V5]; } dt_iop_colorin_params_v5_t; const dt_iop_colorin_params_v5_t *old = (dt_iop_colorin_params_v5_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memset(new, 0, sizeof(*new)); new->type = old->type; dt_strlcpy_fixed_to_fixed(new->filename, sizeof(new->filename), old->filename, sizeof(old->filename)); new->intent = old->intent; new->normalize = old->normalize; new->blue_mapping = old->blue_mapping; new->type_work = old->type_work; dt_strlcpy_fixed_to_fixed(new->filename_work, sizeof(new->filename_work), old->filename_work, sizeof(old->filename_work)); _resolve_work_profile(&new->type_work, new->filename_work); *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 6) { // The structure is equal to to v7 (current) but a new version is // introduced to convert invalid working profile choice to the // default, linear Rec2020. typedef struct dt_iop_colorin_params_v6_t { dt_colorspaces_color_profile_type_t type; char filename[DT_IOP_COLOR_ICC_LEN]; dt_iop_color_intent_t intent; dt_iop_color_normalize_t normalize; gboolean blue_mapping; // working color profile dt_colorspaces_color_profile_type_t type_work; char filename_work[DT_IOP_COLOR_ICC_LEN]; } dt_iop_colorin_params_v6_t; const dt_iop_colorin_params_v6_t *old = (dt_iop_colorin_params_v6_t *)old_params; dt_iop_colorin_params_v7_t *new = malloc(sizeof(dt_iop_colorin_params_v7_t)); memcpy(new, old, sizeof(*new)); // stored params need not terminate the file names new->filename[sizeof(new->filename) - 1] = '\0'; new->filename_work[sizeof(new->filename_work) - 1] = '\0'; _resolve_work_profile(&new->type_work, new->filename_work); *new_params = new; *new_params_size = sizeof(dt_iop_colorin_params_v7_t); *new_version = 7; return 0; } if(old_version == 7) { dt_iop_colorin_params_t *new = calloc(1, sizeof(*new)); memcpy(new, old_params, sizeof(dt_iop_colorin_params_v7_t)); // existing edits retain the Lab output and its original processing paths new->output_rgb = FALSE; *new_params = new; *new_params_size = sizeof(*new); *new_version = 8; return 0; } return 1; #undef DT_IOP_COLOR_ICC_LEN_V5 } void init_global(dt_iop_module_so_t *self) { const int program = 2; // basic.cl, from programs.conf dt_iop_colorin_global_data_t *gd = malloc(sizeof(dt_iop_colorin_global_data_t)); self->data = gd; gd->kernel_colorin_unbound = dt_opencl_create_kernel(program, "colorin_unbound"); gd->kernel_colorin_clipping = dt_opencl_create_kernel(program, "colorin_clipping"); gd->kernel_colorin_rgb = dt_opencl_create_kernel(program, "colorin_rgb"); gd->kernel_colorin_clut = dt_opencl_create_kernel(program, "colorin_clut"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_colorin_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_colorin_unbound); dt_opencl_free_kernel(gd->kernel_colorin_clipping); dt_opencl_free_kernel(gd->kernel_colorin_rgb); dt_opencl_free_kernel(gd->kernel_colorin_clut); free(self->data); self->data = NULL; } static void _profile_changed(GtkWidget *widget, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_request_focus(self); dt_iop_colorin_params_t *p = self->params; dt_iop_colorin_gui_data_t *g = self->gui_data; int pos = dt_bauhaus_combobox_get(widget); GList *prof; if(pos < g->n_image_profiles) prof = g->image_profiles; else { prof = darktable.color_profiles->profiles; pos -= g->n_image_profiles; } for(; prof; prof = g_list_next(prof)) { dt_colorspaces_color_profile_t *pp = prof->data; if(pp->in_pos == pos) { const gboolean old_look = p->type == DT_COLORSPACE_FORWARD_MATRIX || p->type == DT_COLORSPACE_DNG_LOOK; const gboolean new_look = pp->type == DT_COLORSPACE_FORWARD_MATRIX || pp->type == DT_COLORSPACE_DNG_LOOK; p->type = pp->type; memcpy(p->filename, pp->filename, sizeof(p->filename)); dt_dev_add_history_item(darktable.develop, self, TRUE); // top-only sync would leave dng_look's enablement stale on the other pipes if(old_look != new_look) dt_dev_pipe_synch_all(self->dev); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_INPUT); return; } } // should really never happen. dt_print(DT_DEBUG_ALWAYS, "[colorin] color profile %s seems to have disappeared!", dt_colorspaces_get_name(p->type, p->filename)); } static void _workicc_changed(GtkWidget *widget, dt_iop_module_t *self) { dt_iop_colorin_params_t *p = self->params; DT_GUARD_GUI_UPDATE(); dt_iop_request_focus(self); dt_colorspaces_color_profile_type_t type_work = DT_COLORSPACE_NONE; char filename_work[DT_IOP_COLOR_ICC_LEN]; const int pos = dt_bauhaus_combobox_get(widget); for(const GList *prof = darktable.color_profiles->profiles; prof; prof = g_list_next(prof)) { dt_colorspaces_color_profile_t *pp = prof->data; if(pp->work_pos == pos) { type_work = pp->type; dt_strlcpy_to_fixed(filename_work, pp->filename, sizeof(filename_work)); break; } } if(type_work != DT_COLORSPACE_NONE) { p->type_work = type_work; dt_strlcpy_to_fixed(p->filename_work, filename_work, sizeof(p->filename_work)); const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_add_profile_info_to_list(self->dev, p->type_work, p->filename_work, DT_INTENT_PERCEPTUAL); if(work_profile == NULL || !dt_is_valid_colormatrix(work_profile->matrix_in[0][0]) || !dt_is_valid_colormatrix(work_profile->matrix_out[0][0])) { dt_print(DT_DEBUG_ALWAYS, "[colorin] can't extract matrix from colorspace `%s'," " it will be replaced by Rec2020 RGB!", p->filename_work); dt_control_log(_("can't extract matrix from colorspace `%s'" ", it will be replaced by Rec2020 RGB!"), p->filename_work); } dt_dev_add_history_item(darktable.develop, self, TRUE); DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_CONTROL_PROFILE_USER_CHANGED, DT_COLORSPACES_PROFILE_TYPE_WORK); dt_dev_pixelpipe_rebuild(self->dev); } else { // should really never happen. dt_print(DT_DEBUG_ALWAYS, "[colorin] color profile %s seems to have disappeared!", dt_colorspaces_get_name(p->type_work, p->filename_work)); } } static float lerp_lut(const float *const lut, const float v) { // TODO: check if optimization is worthwhile! const float ft = CLAMPS(v * (LUT_SAMPLES - 1), 0, LUT_SAMPLES - 1); const int t = ft < LUT_SAMPLES - 2 ? ft : LUT_SAMPLES - 2; const float f = ft - t; const float l1 = lut[t]; const float l2 = lut[t + 1]; return l1 * (1.0f - f) + l2 * f; } // call only if sure that v<1.0 //TODO: dedup this and colorout.c static inline float _lerp_lut(const float *const lut, const float v) { const float z = MAX(v,0.0f); // clip away negatives const float ft = z * (LUT_SAMPLES - 1); // because v<1.0, ft must be less than (LUT_SAMPLES-1), so truncating // will set t <= (LUT_SAMPLES-2) and thus we don't need to clamp it // to avoid an array overrun const int t = (int)ft; const float f = ft - t; const float l1 = lut[t]; const float l2 = lut[t + 1]; return l1 * (1.0f - f) + l2 * f; } static inline void _apply_tone_curves(dt_aligned_pixel_t pixel, const dt_iop_colorin_data_t *const d) { if(d->hdr_transfer) { dt_hdr_decode(pixel, d->hdr_transfer); } else { // assures unbounded color management without extrapolation. Should not be called // for linear profiles, as there is no need to apply a tone curve to them. for(int c = 0; c < 3; c++) if(d->lut[c][0] >= 0.0f) { if(__builtin_expect(pixel[c] < 1.0f, 1)) pixel[c] = _lerp_lut(d->lut[c], pixel[c]); else pixel[c] = dt_iop_eval_exp(d->unbounded_coeffs[c], pixel[c]); } } } #ifdef HAVE_OPENCL // sample xform on a CLUT_LEVEL^3 grid over RGB input values 0..1, the first // channel varying fastest as in the lut3d module. The grid is linear: the // LittleCMS transforms that get here (LUT input profiles) take scene-linear // camera values and clip them to 0..1 themselves. static float *_bake_clut(cmsHTRANSFORM xform) { const size_t level = CLUT_LEVEL; const size_t nodes = level * level * level; float *const rgb = dt_alloc_align_float(4 * nodes); float *const out = dt_alloc_align_float(4 * nodes); float *const clut = dt_alloc_align_float(3 * nodes); if(!rgb || !out || !clut) { dt_free_align(rgb); dt_free_align(out); dt_free_align(clut); return NULL; } DT_OMP_FOR() for(size_t k = 0; k < nodes; k++) { rgb[4 * k + 0] = (float)(k % level) / (float)(level - 1); rgb[4 * k + 1] = (float)((k / level) % level) / (float)(level - 1); rgb[4 * k + 2] = (float)(k / (level * level)) / (float)(level - 1); rgb[4 * k + 3] = 0.0f; } const size_t chunksize = dt_cacheline_chunks(nodes, dt_get_num_threads()); DT_OMP_FOR() for(size_t chunkstart = 0; chunkstart < nodes; chunkstart += chunksize) cmsDoTransform(xform, rgb + 4 * chunkstart, out + 4 * chunkstart, MIN(chunkstart + chunksize, nodes) - chunkstart); DT_OMP_FOR() for(size_t k = 0; k < nodes; k++) for(int c = 0; c < 3; c++) clut[3 * k + c] = out[4 * k + c]; dt_free_align(rgb); dt_free_align(out); return clut; } int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_colorin_data_t *d = piece->data; const dt_iop_colorin_global_data_t *gd = self->global_data; dt_dev_pixelpipe_t *pipe = piece->pipe; const int devid = pipe->devid; const int width = roi_in->width; const int height = roi_in->height; if(d->type == DT_COLORSPACE_LAB && !d->output_rgb) { const size_t region[2] = { width, height }; return dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, region); } const dt_dev_wb_t *wb = &pipe->wb; const gboolean corrected = dt_dev_wb_late_correction(wb, &pipe->dsc) && d->type != DT_COLORSPACE_LAB; dt_aligned_pixel_t coeffs; dt_dev_wb_get_rgb_correction(wb, corrected, coeffs); if(corrected) { for_four_channels(k) { pipe->dsc.temperature.coeffs[k] = wb->D65coeffs[k]; // note: tiling takes care about processed_maximum pipe->dsc.processed_maximum[k] *= coeffs[k]; } dt_print_pipe(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE, "coeff correction", pipe, self, devid, roi_in, roi_out, "`%s' %.3f(*%.3f) %.3f(*%.3f) %.3f(*%.3f)", dt_colorspaces_get_name(d->type, NULL), pipe->dsc.temperature.coeffs[0], coeffs[0], pipe->dsc.temperature.coeffs[1], coeffs[1], pipe->dsc.temperature.coeffs[2], coeffs[2]); } // the LittleCMS path as a 3D LUT, as commit_params() allowed it // Lab input has no matrix either, but no transform: colorin_rgb handles it if(!dt_is_valid_colormatrix(d->cmatrix[0][0]) && d->type != DT_COLORSPACE_LAB) { if(!d->clut) d->clut = _bake_clut(d->xform_cam_out); if(!d->clut) return CL_MEM_OBJECT_ALLOCATION_FAILURE; const int level = CLUT_LEVEL; cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE; cl_mem dev_clut = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3 * level * level * level, d->clut); cl_mem dev_corr = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 4, coeffs); if(dev_clut && dev_corr) err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorin_clut, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_clut), CLARG(level), CLARG(dev_corr)); dt_opencl_release_mem_object(dev_clut); dt_opencl_release_mem_object(dev_corr); return err; } cl_mem dev_m = NULL, dev_l = NULL, dev_r = NULL; cl_mem dev_g = NULL, dev_b = NULL, dev_coeffs = NULL; cl_mem dev_corr = NULL; cl_mem dev_hsm = NULL, dev_work = NULL, dev_work_lut = NULL; dt_colorspaces_iccprofile_info_cl_t *work_cl = NULL; cl_float *work_lut = NULL; int kernel; float cmat[9], lmat[9]; if(d->output_rgb) { kernel = gd->kernel_colorin_rgb; if(d->type == DT_COLORSPACE_LAB) { memset(cmat, 0, sizeof(cmat)); memset(lmat, 0, sizeof(lmat)); } else { pack_3xSSE_to_3x3(d->nrgb ? d->nmatrix : d->rgb_matrix, cmat); pack_3xSSE_to_3x3(d->rgb_lmatrix, lmat); } } else if(d->nrgb) { kernel = gd->kernel_colorin_clipping; pack_3xSSE_to_3x3(d->nmatrix, cmat); pack_3xSSE_to_3x3(d->lmatrix, lmat); } else { kernel = gd->kernel_colorin_unbound; pack_3xSSE_to_3x3(d->cmatrix, cmat); pack_3xSSE_to_3x3(d->lmatrix, lmat); } cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE; if(d->output_rgb) { err = dt_ioppr_build_iccprofile_params_cl(d->work_profile, devid, &work_cl, &work_lut, &dev_work, &dev_work_lut); if(err != CL_SUCCESS) goto error; err = CL_MEM_OBJECT_ALLOCATION_FAILURE; } const gboolean blue_mapping = d->blue_mapping && dt_image_is_matrix_correction_supported(&pipe->image); dev_corr = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 4, coeffs); if(dev_corr == NULL) goto error; dev_m = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 9, cmat); if(dev_m == NULL) goto error; dev_l = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 9, lmat); if(dev_l == NULL) goto error; dev_r = dt_opencl_copy_host_to_image(devid, d->lut[0], 256, 256, sizeof(float)); if(dev_r == NULL) goto error; dev_g = dt_opencl_copy_host_to_image(devid, d->lut[1], 256, 256, sizeof(float)); if(dev_g == NULL) goto error; dev_b = dt_opencl_copy_host_to_image(devid, d->lut[2], 256, 256, sizeof(float)); if(dev_b == NULL) goto error; dev_coeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3 * 3, (float *)d->unbounded_coeffs); if(dev_coeffs == NULL) goto error; const float scale = dt_iop_get_processed_maximum(piece); const int use_hsm = d->hsm != NULL; float dummy_hsm = 0.0f; const size_t len = use_hsm ? (size_t)d->hue_div * d->sat_div * d->val_div * 3 : 1; dev_hsm = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * len, use_hsm ? d->hsm : &dummy_hsm); if(dev_hsm == NULL) goto error; if(d->output_rgb) { const int clipping = d->nrgb != NULL; const int lab_input = d->type == DT_COLORSPACE_LAB; err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_m), CLARG(dev_l), CLARG(dev_r), CLARG(dev_g), CLARG(dev_b), CLARG(blue_mapping), CLARG(dev_coeffs), CLARG(dev_corr), CLARG(scale), CLARG(d->hue_div), CLARG(d->sat_div), CLARG(d->val_div), CLARG(dev_hsm), CLARG(d->hsm_encode), CLARG(use_hsm), CLARG(d->hdr_transfer), CLARG(clipping), CLARG(dev_work), CLARG(dev_work_lut), CLARG(lab_input)); } else { err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_m), CLARG(dev_l), CLARG(dev_r), CLARG(dev_g), CLARG(dev_b), CLARG(blue_mapping), CLARG(dev_coeffs), CLARG(dev_corr), CLARG(scale), CLARG(d->hue_div), CLARG(d->sat_div), CLARG(d->val_div), CLARG(dev_hsm), CLARG(d->hsm_encode), CLARG(use_hsm), CLARG(d->hdr_transfer)); } error: dt_ioppr_free_iccprofile_params_cl(&work_cl, &work_lut, &dev_work, &dev_work_lut); dt_opencl_release_mem_object(dev_m); dt_opencl_release_mem_object(dev_l); dt_opencl_release_mem_object(dev_r); dt_opencl_release_mem_object(dev_g); dt_opencl_release_mem_object(dev_b); dt_opencl_release_mem_object(dev_coeffs); dt_opencl_release_mem_object(dev_corr); dt_opencl_release_mem_object(dev_hsm); return err; } #endif static inline void _apply_blue_mapping(const float *const in, float *const out) { out[0] = in[0]; out[1] = in[1]; out[2] = in[2]; const float YY = out[0] + out[1] + out[2]; if(YY > 0.0f) { const float zz = out[2] / YY; const float bound_z = 0.5f, bound_Y = 0.5f; const float amount = 0.11f; if(zz > bound_z) { const float t = (zz - bound_z) / (1.0f - bound_z) * fminf(1.0, YY / bound_Y); out[1] += t * amount; out[2] -= t * amount; } } } // legacy processing (IOP versions 1 and 2, 2014 and earlier) static void _process_cmatrix_bm(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_aligned_pixel_t corr) { const dt_iop_colorin_data_t *const d = piece->data; const int clipping = (d->nrgb != NULL); dt_colormatrix_t cmatrix; transpose_3xSSE(d->cmatrix, cmatrix); dt_colormatrix_t nmatrix; transpose_3xSSE(d->nmatrix, nmatrix); dt_colormatrix_t lmatrix; transpose_3xSSE(d->lmatrix, lmatrix); const size_t npixels = (size_t)roi_out->height * roi_out->width; // dt_print(DT_DEBUG_ALWAYS, "Using cmatrix codepath"); // only color matrix. use our optimized fast path! DT_OMP_FOR(shared(cmatrix, nmatrix, lmatrix)) for(size_t j = 0; j < npixels; j++) { const float *const restrict in = (const float *)ivoid + 4*j; float *const restrict out = (float *)ovoid + 4*j; dt_aligned_pixel_t cam; copy_pixel(cam, in); _apply_tone_curves(cam, d); cam[3] = 0.0f; _apply_blue_mapping(cam, cam); if(!clipping) { dt_aligned_pixel_t _xyz; dt_apply_transposed_color_matrix(cam, cmatrix, _xyz); dt_aligned_pixel_t res; dt_XYZ_to_Lab(_xyz, res); copy_pixel_nontemporal(out, res); } else { dt_aligned_pixel_t nRGB; dt_apply_transposed_color_matrix(cam, nmatrix, nRGB); dt_aligned_pixel_t cRGB; for_each_channel(c) { cRGB[c] = CLAMP(nRGB[c], 0.0f, 1.0f); } dt_aligned_pixel_t XYZ; dt_apply_transposed_color_matrix(cRGB, lmatrix, XYZ); dt_aligned_pixel_t res; dt_XYZ_to_Lab(XYZ, res); copy_pixel_nontemporal(out, res); } } dt_omploop_sfence(); } static void _cmatrix_fastpath_simple(float *const restrict out, const float *const restrict in, size_t npixels, const dt_colormatrix_t cmatrix, const dt_aligned_pixel_t corr) { const dt_aligned_pixel_t cmatrix_row0 = { cmatrix[0][0], cmatrix[1][0], cmatrix[2][0], 0.0f }; const dt_aligned_pixel_t cmatrix_row1 = { cmatrix[0][1], cmatrix[1][1], cmatrix[2][1], 0.0f }; const dt_aligned_pixel_t cmatrix_row2 = { cmatrix[0][2], cmatrix[1][2], cmatrix[2][2], 0.0f }; // this function is called from inside a parallel for loop, so no // need for further parallelization for(size_t k = 0; k < npixels; k++) { dt_aligned_pixel_t cam = {in[4*k] * corr[0], in[4*k+1] * corr[1], in[4*k+2] * corr[2], 1.0f}; dt_aligned_pixel_t res; dt_RGB_to_Lab(cam, cmatrix_row0, cmatrix_row1, cmatrix_row2, res); copy_pixel_nontemporal(out + 4*k, res); } } static inline void _cmatrix_fastpath_clipping(float *const restrict out, const float *const restrict in, size_t npixels, const dt_colormatrix_t nmatrix, const dt_colormatrix_t lmatrix, const dt_aligned_pixel_t corr) { const dt_aligned_pixel_t nmatrix_row0 = { nmatrix[0][0], nmatrix[1][0], nmatrix[2][0], 0.0f }; const dt_aligned_pixel_t nmatrix_row1 = { nmatrix[0][1], nmatrix[1][1], nmatrix[2][1], 0.0f }; const dt_aligned_pixel_t nmatrix_row2 = { nmatrix[0][2], nmatrix[1][2], nmatrix[2][2], 0.0f }; const dt_aligned_pixel_t lmatrix_row0 = { lmatrix[0][0], lmatrix[1][0], lmatrix[2][0], 0.0f }; const dt_aligned_pixel_t lmatrix_row1 = { lmatrix[0][1], lmatrix[1][1], lmatrix[2][1], 0.0f }; const dt_aligned_pixel_t lmatrix_row2 = { lmatrix[0][2], lmatrix[1][2], lmatrix[2][2], 0.0f }; // this function is called from inside a parallel for loop, so no // need for further parallelization for(size_t k = 0; k < npixels; k++) { dt_aligned_pixel_t cam = {in[4*k] * corr[0], in[4*k+1] * corr[1], in[4*k+2] * corr[2], 1.0f}; dt_aligned_pixel_t nRGB; dt_apply_color_matrix_by_row(cam, nmatrix_row0, nmatrix_row1, nmatrix_row2, nRGB); dt_vector_clip(nRGB); dt_aligned_pixel_t res; dt_RGB_to_Lab(nRGB, lmatrix_row0, lmatrix_row1, lmatrix_row2, res); copy_pixel_nontemporal(out + 4*k, res); } } static void process_cmatrix_fastpath(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_aligned_pixel_t corr) { const dt_iop_colorin_data_t *const d = piece->data; assert(piece->colors == 4); const gboolean clipping = (d->nrgb != NULL); const size_t npixels = (size_t)roi_out->width * roi_out->height; const float *const restrict in = (float*)ivoid; float *const restrict out = (float*)ovoid; // figure out the number of pixels each thread needs to process, // rounded up to a multiple of the CPU's cache line size #ifdef _OPENMP const size_t nthreads = dt_get_num_threads(); const size_t chunksize = dt_cacheline_chunks(npixels, nthreads); DT_OMP_FOR() for(size_t chunk = 0; chunk < nthreads; chunk++) { size_t start = chunksize * chunk; if(start >= npixels) continue; // handle case when chunksize is < 4*nthreads and last thread has no work size_t end = MIN(start + chunksize, npixels); if(clipping) _cmatrix_fastpath_clipping(out + 4*start, in + 4*start, end-start, d->nmatrix, d->lmatrix, corr); else _cmatrix_fastpath_simple(out + 4*start, in + 4*start, end-start, d->cmatrix, corr); } #else // no OpenMP if(clipping) _cmatrix_fastpath_clipping(out, in, npixels, d->nmatrix, d->lmatrix, corr); else _cmatrix_fastpath_simple(out, in, npixels, d->cmatrix, corr); #endif // ensure that all nontemporal writes have been flushed to RAM before we return dt_omploop_sfence(); } static void _cmatrix_proper_simple(float *const restrict out, const float *const restrict in, size_t npixels, const dt_iop_colorin_data_t *const d, const dt_colormatrix_t cmatrix, const dt_aligned_pixel_t corr) { const dt_aligned_pixel_t cmatrix_row0 = { cmatrix[0][0], cmatrix[1][0], cmatrix[2][0], 0.0f }; const dt_aligned_pixel_t cmatrix_row1 = { cmatrix[0][1], cmatrix[1][1], cmatrix[2][1], 0.0f }; const dt_aligned_pixel_t cmatrix_row2 = { cmatrix[0][2], cmatrix[1][2], cmatrix[2][2], 0.0f }; // this function is called from inside a parallel for loop, so no // need for further parallelization for(size_t k = 0; k < npixels; k++) { dt_aligned_pixel_t cam = {in[4*k] * corr[0], in[4*k+1] * corr[1], in[4*k+2] * corr[2], 1.0f}; _apply_tone_curves(cam, d); dt_aligned_pixel_t res; dt_RGB_to_Lab(cam, cmatrix_row0, cmatrix_row1, cmatrix_row2, res); copy_pixel_nontemporal(out + 4*k, res); } } static inline void _cmatrix_proper_clipping(float *const restrict out, const float *const restrict in, size_t npixels, const dt_iop_colorin_data_t *const d, const dt_colormatrix_t nmatrix, const dt_colormatrix_t lmatrix, const dt_aligned_pixel_t corr) { const dt_aligned_pixel_t nmatrix_row0 = { nmatrix[0][0], nmatrix[1][0], nmatrix[2][0], 0.0f }; const dt_aligned_pixel_t nmatrix_row1 = { nmatrix[0][1], nmatrix[1][1], nmatrix[2][1], 0.0f }; const dt_aligned_pixel_t nmatrix_row2 = { nmatrix[0][2], nmatrix[1][2], nmatrix[2][2], 0.0f }; const dt_aligned_pixel_t lmatrix_row0 = { lmatrix[0][0], lmatrix[1][0], lmatrix[2][0], 0.0f }; const dt_aligned_pixel_t lmatrix_row1 = { lmatrix[0][1], lmatrix[1][1], lmatrix[2][1], 0.0f }; const dt_aligned_pixel_t lmatrix_row2 = { lmatrix[0][2], lmatrix[1][2], lmatrix[2][2], 0.0f }; // this function is called from inside a parallel for loop, so no need for further parallelization for(size_t k = 0; k < npixels; k++) { dt_aligned_pixel_t cam = {in[4*k] * corr[0], in[4*k+1] * corr[1], in[4*k+2] * corr[2], 1.0f}; _apply_tone_curves(cam, d); // convert to the gamut-clipping colorspace dt_aligned_pixel_t nRGB; dt_apply_color_matrix_by_row(cam, nmatrix_row0, nmatrix_row1, nmatrix_row2, nRGB); // clip to the gamut colorspace dt_vector_clip(nRGB); // convert from gamut colorspace to destination colorspace dt_aligned_pixel_t res; dt_RGB_to_Lab(nRGB, lmatrix_row0, lmatrix_row1, lmatrix_row2, res); copy_pixel_nontemporal(out + 4*k, res); } } static void process_cmatrix_proper(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_aligned_pixel_t corr) { const dt_iop_colorin_data_t *const d = piece->data; assert(piece->colors == 4); const gboolean clipping = (d->nrgb != NULL); const size_t npixels = (size_t)roi_out->width * roi_out->height; const float *const restrict in = (float*)ivoid; float *const restrict out = (float*)ovoid; // figure out the number of pixels each thread needs to process, // rounded up to a multiple of the CPU's cache line size #ifdef _OPENMP const size_t nthreads = dt_get_num_threads(); const size_t chunksize = dt_cacheline_chunks(npixels, nthreads); DT_OMP_FOR() for(size_t chunk = 0; chunk < nthreads; chunk++) { size_t start = chunksize * chunk; if(start >= npixels) continue; // handle case when chunksize is < 4*nthreads and last thread has no work size_t end = MIN(start + chunksize, npixels); if(clipping) _cmatrix_proper_clipping(out + 4*start, in + 4*start, end-start, d, d->nmatrix, d->lmatrix, corr); else _cmatrix_proper_simple(out + 4*start, in + 4*start, end-start, d, d->cmatrix, corr); } #else if(clipping) _cmatrix_proper_clipping(out, in, npixels, d,d->nmatrix, d->lmatrix, corr); else _cmatrix_proper_simple(out, in, npixels, d, d->cmatrix, corr); #endif // ensure that all nontemporal writes have been flushed to RAM before we return dt_omploop_sfence(); } static void process_cmatrix(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_aligned_pixel_t corr) { const dt_iop_colorin_data_t *const d = piece->data; const gboolean blue_mapping = d->blue_mapping && dt_image_is_matrix_correction_supported(&piece->pipe->image); if(!blue_mapping && !d->nonlinearlut) { process_cmatrix_fastpath(self, piece, ivoid, ovoid, roi_in, roi_out, corr); } else if(blue_mapping) { _process_cmatrix_bm(self, piece, ivoid, ovoid, roi_in, roi_out, corr); } else { process_cmatrix_proper(self, piece, ivoid, ovoid, roi_in, roi_out, corr); } } // legacy processing (IOP versions 1 and 2, 2014 and earlier) static void process_lcms2_bm(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { const dt_iop_colorin_data_t *const d = piece->data; const size_t height = roi_out->height; const size_t width = roi_out->width; // use general lcms2 fallback DT_OMP_FOR() for(int k = 0; k < height; k++) { const float *in = (const float *)ivoid + (size_t)4 * k * width; float *out = (float *)ovoid + (size_t)4 * k * width; for(int j = 0; j < width; j++) { _apply_blue_mapping(in + 4*j, out + 4*j); } // convert to (L,a/L,b/L) to be able to change L without changing saturation. if(!d->nrgb) { cmsDoTransform(d->xform_cam_out, out, out, width); } else { cmsDoTransform(d->xform_cam_nrgb, out, out, width); for(int j = 0; j < width; j++) dt_vector_clip(&out[4*j]); cmsDoTransform(d->xform_nrgb_out, out, out, width); } } } static void process_lcms2_proper(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_aligned_pixel_t corr) { const dt_iop_colorin_data_t *const d = piece->data; const size_t height = roi_out->height; const size_t width = roi_out->width; size_t padded_size; float *const restrict scratchlines = dt_alloc_perthread_float(4 * width, &padded_size); gboolean correcting = corr[0] != 1.0f || corr[1] != 1.0f || corr[2] != 1.0f; DT_OMP_FOR() for(size_t k = 0; k < height; k++) { float *in = (float *)ivoid + (size_t)4 * k * width; float *const restrict scratch = dt_get_perthread(scratchlines, padded_size); if(correcting || d->output_rgb) { for(size_t x = 0; x < 4 * width; x += 4) { dt_vector_mul(&scratch[x], &in[x], corr); if(d->output_rgb) { if(d->hdr_transfer) dt_hdr_decode(&scratch[x], d->hdr_transfer); if(d->blue_mapping && dt_image_is_matrix_correction_supported(&piece->pipe->image)) _apply_blue_mapping(&scratch[x], &scratch[x]); } } in = scratch; } float *out = (float *)ovoid + (size_t)4 * k * width; // convert to the output profile, clipping in the selected gamut if requested if(!d->nrgb) { cmsDoTransform(d->xform_cam_out, in, out, width); } else { cmsDoTransform(d->xform_cam_nrgb, in, out, width); for(int j = 0; j < width; j++) dt_vector_clip(&out[4*j]); cmsDoTransform(d->xform_nrgb_out, out, out, width); } } dt_free_align(scratchlines); } static void _process_rgb_matrix(const dt_iop_colorin_data_t *const d, const float *const in, float *const out, const size_t npixels, const dt_aligned_pixel_t corr, const gboolean blue_mapping) { dt_colormatrix_t matrix, clipping_matrix; transpose_3xSSE(d->nrgb ? d->nmatrix : d->rgb_matrix, matrix); transpose_3xSSE(d->rgb_lmatrix, clipping_matrix); DT_OMP_FOR(shared(matrix, clipping_matrix)) for(size_t k = 0; k < npixels; k++) { dt_aligned_pixel_t cam, linear; for_each_channel(c) cam[c] = in[4*k+c] * corr[c]; cam[3] = in[4*k+3]; _apply_tone_curves(cam, d); if(blue_mapping) _apply_blue_mapping(cam, cam); dt_apply_transposed_color_matrix(cam, matrix, linear); if(d->nrgb) { dt_vector_clip(linear); dt_apply_transposed_color_matrix(linear, clipping_matrix, cam); copy_pixel(linear, cam); } linear[3] = in[4*k+3]; if(d->hsm) copy_pixel(out + 4*k, linear); else dt_ioppr_encode_rgb(linear, out + 4*k, d->work_profile); } } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; dt_dev_pixelpipe_t *pipe = piece->pipe; const dt_dev_wb_t *wb = &pipe->wb; const dt_iop_colorin_data_t *const d = piece->data; const gboolean corrected = dt_dev_wb_late_correction(wb, &pipe->dsc) && d->type != DT_COLORSPACE_LAB; dt_aligned_pixel_t coeffs; dt_dev_wb_get_rgb_correction(wb, corrected, coeffs); if(corrected) { for_four_channels(k) { pipe->dsc.temperature.coeffs[k] = wb->D65coeffs[k]; // note: tiling takes care about processed_maximum pipe->dsc.processed_maximum[k] *= coeffs[k]; } dt_print_pipe(DT_DEBUG_PIPE | DT_DEBUG_VERBOSE, "coeff correction", pipe, self, DT_DEVICE_CPU, roi_in, roi_out, "`%s' %.3f(*%.3f) %.3f(*%.3f) %.3f(*%.3f)", dt_colorspaces_get_name(d->type, NULL), pipe->dsc.temperature.coeffs[0], coeffs[0], pipe->dsc.temperature.coeffs[1], coeffs[1], pipe->dsc.temperature.coeffs[2], coeffs[2]); } const gboolean blue_mapping = d->blue_mapping && dt_image_is_matrix_correction_supported(&pipe->image); if(d->type == DT_COLORSPACE_LAB) { if(d->output_rgb) { const float *in = ivoid; float *out = ovoid; DT_OMP_FOR() for(size_t k = 0; k < (size_t)roi_out->width * roi_out->height; k++) { dt_aligned_pixel_t xyz, linear; dt_Lab_to_XYZ(in + 4*k, xyz); dt_apply_transposed_color_matrix(xyz, d->work_profile->matrix_out_transposed, linear); linear[3] = in[4*k+3]; dt_ioppr_encode_rgb(linear, out + 4*k, d->work_profile); } } else dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors); } else if(dt_is_valid_colormatrix(d->cmatrix[0][0])) { if(d->output_rgb) _process_rgb_matrix(d, ivoid, ovoid, (size_t)roi_out->width * roi_out->height, coeffs, blue_mapping); else process_cmatrix(self, piece, ivoid, ovoid, roi_in, roi_out, coeffs); } else { // use general lcms2 fallback if(blue_mapping && !d->output_rgb) { process_lcms2_bm(self, piece, ivoid, ovoid, roi_in, roi_out); } else { process_lcms2_proper(self, piece, ivoid, ovoid, roi_in, roi_out, coeffs); } } if(d->output_rgb && !dt_is_valid_colormatrix(d->cmatrix[0][0]) && d->type != DT_COLORSPACE_LAB) { const float *in = ivoid; float *out = ovoid; DT_OMP_FOR() for(size_t k = 0; k < (size_t)roi_out->width * roi_out->height; k++) { if(d->hsm) dt_ioppr_decode_rgb(out + 4*k, out + 4*k, d->work_profile); out[4*k+3] = in[4*k+3]; } } if(d->hsm) { _process_dng_look((float *)ovoid, roi_in->width, roi_in->height, d, dt_iop_get_processed_maximum(piece)); } } void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { const dt_iop_colorin_params_t *p = (dt_iop_colorin_params_t *)p1; dt_iop_colorin_data_t *d = piece->data; d->output_rgb = p->output_rgb; d->work_profile = NULL; d->type = p->type; d->type_work = p->type_work; // stored params need not terminate the file names dt_strlcpy_fixed_to_fixed(d->filename, sizeof(d->filename), p->filename, sizeof(p->filename)); dt_strlcpy_fixed_to_fixed(d->filename_work, sizeof(d->filename_work), p->filename_work, sizeof(p->filename_work)); cmsHPROFILE output = dt_colorspaces_get_profile(DT_COLORSPACE_LAB, "", DT_PROFILE_DIRECTION_ANY)->profile; const unsigned int output_format = d->output_rgb ? TYPE_RGBA_FLT : TYPE_LabA_FLT; if(d->output_rgb) { d->work_profile = dt_ioppr_set_pipe_work_profile_info(self->dev, pipe, d->type_work, d->filename_work, DT_INTENT_PERCEPTUAL); output = dt_colorspaces_get_profile(d->work_profile->type, d->work_profile->filename, DT_PROFILE_DIRECTION_WORK)->profile; } // only clean up when it's a type that we created here if(d->input && d->clear_input) dt_colorspaces_cleanup_profile(d->input); d->input = NULL; d->clear_input = FALSE; d->nrgb = NULL; g_free(d->hsm); d->hsm = NULL; d->hue_div = d->sat_div = d->val_div = d->hsm_encode = 0; d->blue_mapping = p->blue_mapping; switch(p->normalize) { case DT_NORMALIZE_SRGB: d->nrgb = dt_colorspaces_get_profile(DT_COLORSPACE_SRGB, "", DT_PROFILE_DIRECTION_IN)->profile; break; case DT_NORMALIZE_ADOBE_RGB: d->nrgb = dt_colorspaces_get_profile(DT_COLORSPACE_ADOBERGB, "", DT_PROFILE_DIRECTION_IN)->profile; break; case DT_NORMALIZE_LINEAR_REC709_RGB: d->nrgb = dt_colorspaces_get_profile(DT_COLORSPACE_LIN_REC709, "", DT_PROFILE_DIRECTION_IN)->profile; break; case DT_NORMALIZE_LINEAR_REC2020_RGB: d->nrgb = dt_colorspaces_get_profile(DT_COLORSPACE_LIN_REC2020, "", DT_PROFILE_DIRECTION_IN)->profile; break; case DT_NORMALIZE_OFF: default: d->nrgb = NULL; } if(d->xform_cam_out) { cmsDeleteTransform(d->xform_cam_out); d->xform_cam_out = NULL; } if(d->xform_cam_nrgb) { cmsDeleteTransform(d->xform_cam_nrgb); d->xform_cam_nrgb = NULL; } if(d->xform_nrgb_out) { cmsDeleteTransform(d->xform_nrgb_out); d->xform_nrgb_out = NULL; } dt_free_align(d->clut); d->clut = NULL; dt_mark_colormatrix_invalid(&d->cmatrix[0][0]); dt_mark_colormatrix_invalid(&d->nmatrix[0][0]); dt_mark_colormatrix_invalid(&d->lmatrix[0][0]); d->lut[0][0] = -1.0f; d->lut[1][0] = -1.0f; d->lut[2][0] = -1.0f; d->nonlinearlut = FALSE; d->hdr_transfer = DT_HDR_NONE; piece->process_cl_ready = TRUE; dt_colorspaces_color_profile_type_t type = p->type; if(type == DT_COLORSPACE_LAB) // we keep the runtime in process for safety { piece->enabled = d->output_rgb; return; } if(!dt_pipe_is_image_final(piece->pipe)) piece->enabled = TRUE; if(type == DT_COLORSPACE_ENHANCED_MATRIX) { d->input = dt_colorspaces_create_darktable_profile(pipe->image.camera_makermodel); if(!d->input) type = DT_COLORSPACE_EMBEDDED_ICC; else d->clear_input = TRUE; } if(type == DT_COLORSPACE_VENDOR_MATRIX) { d->input = dt_colorspaces_create_vendor_profile(pipe->image.camera_makermodel); if(!d->input) type = DT_COLORSPACE_EMBEDDED_ICC; else d->clear_input = TRUE; } if(type == DT_COLORSPACE_ALTERNATE_MATRIX) { d->input = dt_colorspaces_create_alternate_profile(pipe->image.camera_makermodel); if(!d->input) type = DT_COLORSPACE_EMBEDDED_ICC; else d->clear_input = TRUE; } if(type == DT_COLORSPACE_EMBEDDED_ICC) { // embedded color profile const dt_image_t *cimg = dt_image_cache_get(pipe->image.id, 'r'); if(cimg == NULL || cimg->profile == NULL) type = DT_COLORSPACE_EMBEDDED_MATRIX; else { d->input = dt_colorspaces_get_rgb_profile_from_mem(cimg->profile, cimg->profile_size); d->clear_input = TRUE; } dt_image_cache_read_release(cimg); } if(type == DT_COLORSPACE_FORWARD_MATRIX) { if(dt_is_valid_colormatrix(pipe->image.dng_forward_matrix[0])) { // ForwardMatrix (DNG spec) is already camera -> XYZ, unlike // d65_color_matrix (XYZ -> camera). Do NOT invert it again. d->input = dt_colorspaces_create_xyzmatrix_profile((const float(*)[3])pipe->image.dng_forward_matrix); d->clear_input = TRUE; _commit_hsm(d, pipe->image.id); } else type = DT_COLORSPACE_EMBEDDED_MATRIX; } if(type == DT_COLORSPACE_EMBEDDED_MATRIX || type == DT_COLORSPACE_DNG_LOOK) { // embedded matrix, hopefully D65 if(dt_is_valid_colormatrix(pipe->image.d65_color_matrix[0])) { d->input = dt_colorspaces_create_xyzimatrix_profile((float(*)[3])pipe->image.d65_color_matrix); d->clear_input = TRUE; if(type == DT_COLORSPACE_DNG_LOOK) { _commit_hsm(d, pipe->image.id); } } else type = DT_COLORSPACE_STANDARD_MATRIX; } if(type == DT_COLORSPACE_STANDARD_MATRIX) { /* Note In rawspeed we set image.adobe_XYZ_to_CAM to zeros thus marking it as valid but we can't do dt_colorspaces_create_xyzimatrix_profile() on this as that involves an undefined matrix inversion leading to undefined matrix parameters. */ gboolean validmat = dt_is_valid_colormatrix(pipe->image.adobe_XYZ_to_CAM[0][0]); if(validmat) { float sum = 0.0f; for(int i = 0; i < 3; i++) for(int j = 0; j < 3; j++) sum += fabsf(pipe->image.adobe_XYZ_to_CAM[i][j]); validmat = sum != 0.0f; } if(!validmat) { if(dt_image_is_matrix_correction_supported(&pipe->image)) { dt_print(DT_DEBUG_ALWAYS, "[colorin] `%s' color matrix not found, !", pipe->image.camera_makermodel); dt_control_log(_("`%s' color matrix not found!"), pipe->image.camera_makermodel); } type = DT_COLORSPACE_LIN_REC709; } else { d->input = dt_colorspaces_create_xyzimatrix_profile((float(*)[3])pipe->image.adobe_XYZ_to_CAM); d->clear_input = TRUE; } } if(!d->input) { const dt_colorspaces_color_profile_t *profile = dt_colorspaces_get_profile(type, d->filename, DT_PROFILE_DIRECTION_IN); if(profile) d->input = profile->profile; } if(!d->input && type != DT_COLORSPACE_SRGB) { // use linear_rec709_rgb as fallback for missing non-sRGB profiles: d->input = dt_colorspaces_get_profile(DT_COLORSPACE_LIN_REC709, "", DT_PROFILE_DIRECTION_IN)->profile; d->clear_input = FALSE; } // final resort: sRGB if(!d->input) { d->input = dt_colorspaces_get_profile(DT_COLORSPACE_SRGB, "", DT_PROFILE_DIRECTION_IN)->profile; d->clear_input = FALSE; } // should never happen, but catch that case to avoid a crash if(!d->input) { dt_print(DT_DEBUG_ALWAYS, "[colorin] input profile could not be generated!"); dt_control_log(_("input profile could not be generated!")); piece->enabled = FALSE; return; } d->hdr_transfer = dt_colorspaces_hdr_transfer(type); if(d->hdr_transfer) { cmsHPROFILE linear = dt_colorspaces_linearize_profile(d->input); if(!linear) { piece->enabled = FALSE; return; } d->input = linear; d->clear_input = TRUE; } cmsColorSpaceSignature input_color_space = cmsGetColorSpace(d->input); cmsUInt32Number input_format; switch(input_color_space) { case cmsSigRgbData: input_format = TYPE_RGBA_FLT; break; case cmsSigXYZData: // FIXME: even though this is allowed/works, // dt_ioppr_generate_profile_info still complains about these // profiles input_format = TYPE_XYZA_FLT; break; default: dt_print(DT_DEBUG_ALWAYS, "[colorin] input profile color space `%c%c%c%c' not supported", (char)(input_color_space>>24), (char)(input_color_space>>16), (char)(input_color_space>>8), (char)(input_color_space)); input_format = TYPE_RGBA_FLT; // this will fail later, // triggering the linear rec709 // fallback } // prepare transformation matrix or lcms2 transforms as fallback if(d->nrgb) { // user wants us to clip to a given RGB profile if(dt_colorspaces_get_matrix_from_input_profile (d->input, d->cmatrix, d->lut[0], d->lut[1], d->lut[2], LUT_SAMPLES)) { piece->process_cl_ready = FALSE; dt_mark_colormatrix_invalid(&d->cmatrix[0][0]); d->xform_cam_out = cmsCreateTransform(d->input, input_format, output, output_format, p->intent, 0); d->xform_cam_nrgb = cmsCreateTransform(d->input, input_format, d->nrgb, TYPE_RGBA_FLT, p->intent, 0); d->xform_nrgb_out = cmsCreateTransform(d->nrgb, TYPE_RGBA_FLT, output, output_format, p->intent, 0); } else { float lutr[1], lutg[1], lutb[1]; dt_colormatrix_t omat; dt_colorspaces_get_matrix_from_output_profile(d->nrgb, omat, lutr, lutg, lutb, 1); dt_colormatrix_mul(d->nmatrix, omat, d->cmatrix); dt_colorspaces_get_matrix_from_input_profile(d->nrgb, d->lmatrix, lutr, lutg, lutb, 1); } } else { // default mode: unbound processing if(dt_colorspaces_get_matrix_from_input_profile(d->input, d->cmatrix, d->lut[0], d->lut[1], d->lut[2], LUT_SAMPLES)) { piece->process_cl_ready = FALSE; dt_mark_colormatrix_invalid(&d->cmatrix[0][0]); d->xform_cam_out = cmsCreateTransform(d->input, input_format, output, output_format, p->intent, 0); } } // we might have failed generating the clipping transformations, check that: if(d->nrgb && ((!d->xform_cam_nrgb && !dt_is_valid_colormatrix(d->nmatrix[0][0])) || (!d->xform_nrgb_out && !dt_is_valid_colormatrix(d->lmatrix[0][0])))) { if(d->xform_cam_nrgb) { cmsDeleteTransform(d->xform_cam_nrgb); d->xform_cam_nrgb = NULL; } if(d->xform_nrgb_out) { cmsDeleteTransform(d->xform_nrgb_out); d->xform_nrgb_out = NULL; } d->nrgb = NULL; } // user selected a non-supported input profile, check that: if(!d->xform_cam_out && !dt_is_valid_colormatrix(d->cmatrix[0][0])) { if(p->type == DT_COLORSPACE_FILE) dt_print(DT_DEBUG_ALWAYS, "[colorin] unsupported input profile `%s' has" " been replaced by linear Rec709 RGB!\n", d->filename); else dt_print(DT_DEBUG_ALWAYS, "[colorin] unsupported input profile has been" " replaced by linear Rec709 RGB!\n"); dt_control_log(_("unsupported input profile has been replaced by linear Rec709 RGB!")); if(d->input && d->clear_input) dt_colorspaces_cleanup_profile(d->input); d->nrgb = NULL; d->input = dt_colorspaces_get_profile(DT_COLORSPACE_LIN_REC709, "", DT_PROFILE_DIRECTION_IN)->profile; d->clear_input = FALSE; if(dt_colorspaces_get_matrix_from_input_profile(d->input, d->cmatrix, d->lut[0], d->lut[1], d->lut[2], LUT_SAMPLES)) { piece->process_cl_ready = FALSE; dt_mark_colormatrix_invalid(&d->cmatrix[0][0]); d->xform_cam_out = cmsCreateTransform(d->input, TYPE_RGBA_FLT, output, output_format, p->intent, 0); } } d->nonlinearlut = FALSE; // now try to initialize unbounded mode: // we do a extrapolation for input values above 1.0f. // unfortunately we can only do this if we got the computation // in our hands, i.e. for the fast builtin-dt-matrix-profile path. for(int k = 0; k < 3; k++) { // omit luts marked as linear (negative as marker) if(d->lut[k][0] >= 0.0f) { d->nonlinearlut = TRUE; const float x[4] = { 0.7f, 0.8f, 0.9f, 1.0f }; const float y[4] = { lerp_lut(d->lut[k], x[0]), lerp_lut(d->lut[k], x[1]), lerp_lut(d->lut[k], x[2]), lerp_lut(d->lut[k], x[3]) }; dt_iop_estimate_exp(x, y, 4, d->unbounded_coeffs[k]); } else d->unbounded_coeffs[k][0] = -1.0f; } d->nonlinearlut |= d->hdr_transfer != DT_HDR_NONE; // the GPU runs xform_cam_out as a 3D LUT over RGB input. It has none of // the gamut clipping, blue mapping or HDR decoding process_lcms2_proper() // and process_lcms2_bm() wrap around the transform, so those stay on the CPU. if(d->xform_cam_out && !d->nrgb && !d->blue_mapping && !d->hdr_transfer && !d->hsm && input_format == TYPE_RGBA_FLT) piece->process_cl_ready = TRUE; if(d->output_rgb && dt_is_valid_colormatrix(d->cmatrix[0][0])) { dt_colormatrix_mul(d->rgb_matrix, d->work_profile->matrix_out, d->cmatrix); if(d->nrgb) dt_colormatrix_mul(d->rgb_lmatrix, d->work_profile->matrix_out, d->lmatrix); else memset(d->rgb_lmatrix, 0, sizeof(d->rgb_lmatrix)); } // commit color profiles to pipeline if(!d->output_rgb) dt_ioppr_set_pipe_work_profile_info(self->dev, piece->pipe, d->type_work, d->filename_work, DT_INTENT_PERCEPTUAL); dt_ioppr_set_pipe_input_profile_info(self->dev, piece->pipe, d->type, d->filename, p->intent, d->cmatrix); } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = dt_alloc1_align_type(dt_iop_colorin_data_t); dt_iop_colorin_data_t *d = piece->data; d->output_rgb = FALSE; d->work_profile = NULL; d->input = NULL; d->nrgb = NULL; d->xform_cam_out = NULL; d->xform_cam_nrgb = NULL; d->xform_nrgb_out = NULL; d->clut = NULL; d->hsm = NULL; } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_colorin_data_t *d = piece->data; if(d->input && d->clear_input) dt_colorspaces_cleanup_profile(d->input); if(d->xform_cam_out) { cmsDeleteTransform(d->xform_cam_out); d->xform_cam_out = NULL; } if(d->xform_cam_nrgb) { cmsDeleteTransform(d->xform_cam_nrgb); d->xform_cam_nrgb = NULL; } if(d->xform_nrgb_out) { cmsDeleteTransform(d->xform_nrgb_out); d->xform_nrgb_out = NULL; } g_free(d->hsm); dt_free_align(d->clut); dt_free_align(piece->data); piece->data = NULL; } void gui_update(dt_iop_module_t *self) { dt_iop_colorin_gui_data_t *g = self->gui_data; dt_iop_colorin_params_t *p = self->params; // the GUI handlers read the stored file names as C strings p->filename[sizeof(p->filename) - 1] = '\0'; p->filename_work[sizeof(p->filename_work) - 1] = '\0'; dt_bauhaus_combobox_set(g->clipping_combobox, p->normalize); // working profile int idx = -1; for(const GList *prof = darktable.color_profiles->profiles; prof; prof = g_list_next(prof)) { dt_colorspaces_color_profile_t *pp = prof->data; if(pp->work_pos > -1 && pp->type == p->type_work && (pp->type != DT_COLORSPACE_FILE || dt_colorspaces_is_profile_equal(pp->filename, p->filename_work))) { idx = pp->work_pos; break; } } if(idx < 0) { idx = 0; dt_print(DT_DEBUG_ALWAYS, "[gui colorin] could not find requested working profile `%s'!", dt_colorspaces_get_name(p->type_work, p->filename_work)); } dt_bauhaus_combobox_set(g->work_combobox, idx); for(const GList *prof = g->image_profiles; prof; prof = g_list_next(prof)) { dt_colorspaces_color_profile_t *pp = prof->data; if(pp->type == p->type && (pp->type != DT_COLORSPACE_FILE || dt_colorspaces_is_profile_equal(pp->filename, p->filename))) { dt_bauhaus_combobox_set(g->profile_combobox, pp->in_pos); return; } } for(const GList *prof = darktable.color_profiles->profiles; prof; prof = g_list_next(prof)) { dt_colorspaces_color_profile_t *pp = prof->data; if(pp->in_pos > -1 && pp->type == p->type && (pp->type != DT_COLORSPACE_FILE || dt_colorspaces_is_profile_equal(pp->filename, p->filename))) { dt_bauhaus_combobox_set(g->profile_combobox, pp->in_pos + g->n_image_profiles); return; } } dt_bauhaus_combobox_set(g->profile_combobox, 0); dt_print(DT_DEBUG_PIPE, "[gui colorin] using default instead of `%s'", dt_colorspaces_get_name(p->type, p->filename)); } // FIXME: update the gui when we add/remove the eprofile or ematrix void reload_defaults(dt_iop_module_t *self) { self->default_enabled = TRUE; self->hide_enable_button = TRUE; dt_iop_colorin_params_t *d = self->default_params; dt_colorspaces_color_profile_type_t color_profile = DT_COLORSPACE_NONE; // some file formats like jpeg can have an embedded color profile // currently we only support jpeg, j2k, tiff, png, avif, and heif dt_image_t *img = dt_image_cache_get(self->dev->image_storage.id, 'w'); char filename[PATH_MAX] = { 0 }; gboolean from_cache = TRUE; dt_image_full_path(img->id, filename, sizeof(filename), &from_cache); const char *extension = strrchr(filename, '.'); if(extension && !g_ascii_strcasecmp(extension, ".png")) { /* PNG cICP has priority over ICC, including after the ICC was cached. */ dt_colorspaces_cicp_t cicp; uint8_t *profile = NULL; const int size = dt_imageio_png_read_profile(filename, &profile, &cicp); if(!img->profile) { img->profile = profile; img->profile_size = size; } else g_free(profile); color_profile = dt_colorspaces_cicp_to_type(&cicp, filename); if(color_profile == DT_COLORSPACE_NONE && img->profile_size > 0) color_profile = DT_COLORSPACE_EMBEDDED_ICC; } if(!img->profile) { // the image has not a profile inited const gchar *cc = filename + strlen(filename); for(; *cc != '.' && cc > filename; cc--) ; gchar *ext = g_ascii_strdown(cc + 1, -1); if(!strcmp(ext, "jpg") || !strcmp(ext, "jpeg")) { dt_imageio_jpeg_t jpg; if(!dt_imageio_jpeg_read_header(filename, &jpg)) { img->profile_size = dt_imageio_jpeg_read_profile(&jpg, &img->profile); color_profile = (img->profile_size > 0) ? DT_COLORSPACE_EMBEDDED_ICC : DT_COLORSPACE_NONE; } } else if(!strcmp(ext, "pfm")) { // PFM have no embedded color profile nor ICC tag, we can't know the color space // but we can assume the are linear since it's a floating point format color_profile = DT_COLORSPACE_LIN_REC709; } // the ldr test just checks for magics in the file header else if((!strcmp(ext, "tif") || !strcmp(ext, "tiff")) && dt_imageio_is_ldr(filename)) { img->profile_size = dt_imageio_tiff_read_profile(filename, &img->profile); color_profile = (img->profile_size > 0) ? DT_COLORSPACE_EMBEDDED_ICC : DT_COLORSPACE_NONE; } #ifdef HAVE_LIBAVIF else if(!strcmp(ext, "avif")) { dt_colorspaces_cicp_t cicp; img->profile_size = dt_imageio_avif_read_profile(filename, &img->profile, &cicp); /* AVIF spec gives priority to ICC profile over CICP; only one * valid kind is returned above anyway */ color_profile = (img->profile_size > 0) ? DT_COLORSPACE_EMBEDDED_ICC : dt_colorspaces_cicp_to_type(&cicp, filename); } #endif #ifdef HAVE_LIBHEIF else if(!strcmp(ext, "heif") || !strcmp(ext, "heic") || !strcmp(ext, "hej2") || !strcmp(ext, "avci") || !strcmp(ext, "hif") #ifndef HAVE_LIBAVIF || !strcmp(ext, "avif") #endif ) { dt_colorspaces_cicp_t cicp; img->profile_size = dt_imageio_heif_read_profile(filename, &img->profile, &cicp); /* HEIF spec gives priority to ICC profile over CICP; only one valid kind is returned above anyway */ color_profile = (img->profile_size > 0) ? DT_COLORSPACE_EMBEDDED_ICC : dt_colorspaces_cicp_to_type(&cicp, filename); } #endif g_free(ext); } else { // Keep any PNG cICP selected above when its ICC profile was cached. if(color_profile == DT_COLORSPACE_NONE) color_profile = DT_COLORSPACE_EMBEDDED_ICC; } #ifdef HAVE_LIBJXL /* A synthesized ICC shaper cannot express absolute PQ luminance or the RGB-dependent HLG OOTF. Prefer structured HDR metadata when present. */ const char *ext = strrchr(filename, '.'); if(ext && !g_ascii_strcasecmp(ext, ".jxl")) { const dt_colorspaces_color_profile_type_t hdr = dt_imageio_jpegxl_read_hdr_profile(filename); if(hdr != DT_COLORSPACE_NONE) color_profile = hdr; } #endif // We'll update the input profile hint with information on the // embedded ICC profile if it exists. Since the tooltip info is now // image dependent, we need to set the tooltip for each image // change. // We need gui_data to access widget in order to change tooltip. dt_iop_colorin_gui_data_t *g = self->gui_data; // reload_defaults() can be called with unavailable (i.e., NULL) gui_data. // In this case, we have nothing to do with tooltips. if(g) { gboolean profile_embedded_but_corrupted = FALSE; char *tooltip_part_profile_dirs = dt_ioppr_get_location_tooltip("in", _("external ICC profiles")); // In case of embedded ICC profile we will modify tooltip with the // profile info, otherwise reset tooltip to generic text. if(color_profile == DT_COLORSPACE_EMBEDDED_ICC) { cmsHPROFILE cmsprofile = cmsOpenProfileFromMem(img->profile, img->profile_size); // So, exiv2 read the embedded ICC profile from the image file, but it // can be corrupted to the point that lcms2 cannot open it as a profile. // In such a case, we have to bail out of the profile reading code, as // we cannot read the profile properties and change the tooltip. if(!cmsprofile) { profile_embedded_but_corrupted = TRUE; dt_print(DT_DEBUG_ALWAYS, "[colorin] ICC profile is embedded but corrupted"); goto corrupted_profile; } char iccDesc[64]; iccDesc[0] = '\0'; cmsGetProfileInfoASCII(cmsprofile, cmsInfoDescription, "en", "US", iccDesc, 64); char iccManuf[64]; iccManuf[0] = '\0'; cmsGetProfileInfoASCII(cmsprofile, cmsInfoManufacturer, "en", "US", iccManuf, 64); char iccModel[64]; iccModel[0] = '\0'; cmsGetProfileInfoASCII(cmsprofile, cmsInfoModel, "en", "US", iccModel, 64); // This is the only profile field in which, although it usually // contains a short copyright text, in theory profile creators can // put as long text as they want. So, we can't take a fast // approach of statically allocating memory of some sufficient // size and have to find out the size of the field at run-time and // dynamically allocate memory. char* iccCopyr; const guint32 bufsize = cmsGetProfileInfoASCII(cmsprofile, cmsInfoCopyright, "en", "US", NULL, 0); if(bufsize && (iccCopyr = malloc(bufsize+1)) != NULL) { cmsGetProfileInfoASCII(cmsprofile, cmsInfoCopyright, "en", "US", iccCopyr, bufsize); } else { iccCopyr = ""; } const guint8 iccMajorVersion = cmsGetEncodedICCversion(cmsprofile) >> 24; const guint8 iccMinorVersion = (cmsGetEncodedICCversion(cmsprofile) << 8) >> 28; char *iccType = ""; if(cmsIsMatrixShaper(cmsprofile)) iccType = "Matrix"; else if(cmsIsCLUT(cmsprofile, INTENT_PERCEPTUAL, LCMS_USED_AS_INPUT)) iccType = "LUT"; char *tooltip = g_markup_printf_escaped(_("embedded ICC profile properties:\n\n" "name: %s\n" "version: %d.%d\n" "type: %s\n" "manufacturer: %s\n" "model: %s\n" "copyright: %s\n\n"), iccDesc, iccMajorVersion, iccMinorVersion, iccType, iccManuf, iccModel, iccCopyr); char *tooltip2 = g_strconcat(tooltip, tooltip_part_profile_dirs, NULL); gtk_widget_set_tooltip_markup(g->profile_combobox, tooltip2); g_free(tooltip); g_free(tooltip2); g_free(tooltip_part_profile_dirs); if(bufsize) free(iccCopyr); } // Make a generic tooltip if there is no ICC profile in the current image, // or we jumped here because profile is corrupted. corrupted_profile: if((color_profile != DT_COLORSPACE_EMBEDDED_ICC) || profile_embedded_but_corrupted) { gtk_widget_set_tooltip_markup(g->profile_combobox, tooltip_part_profile_dirs); g_free(tooltip_part_profile_dirs); } } if(color_profile != DT_COLORSPACE_NONE) d->type = color_profile; else if(img->flags & DT_IMAGE_4BAYER) // 4Bayer images have been pre-converted to rec2020 d->type = DT_COLORSPACE_LIN_REC2020; else if(dt_image_is_monochrome(img)) d->type = DT_COLORSPACE_LIN_REC709; else if(img->colorspace == DT_IMAGE_COLORSPACE_SRGB) d->type = DT_COLORSPACE_SRGB; else if(img->colorspace == DT_IMAGE_COLORSPACE_ADOBE_RGB) d->type = DT_COLORSPACE_ADOBERGB; else if(dt_image_is_ldr(img)) d->type = DT_COLORSPACE_SRGB; else if(dt_is_valid_colormatrix(img->d65_color_matrix[0])) // image is DNG, EXR, or RGBE d->type = DT_COLORSPACE_EMBEDDED_MATRIX; else if(dt_is_valid_colormatrix(self->dev->image_storage.dng_forward_matrix[0])) // DNG with forward matrix d->type = DT_COLORSPACE_FORWARD_MATRIX; else if(dt_image_is_matrix_correction_supported(img)) // image is raw d->type = DT_COLORSPACE_STANDARD_MATRIX; else if(dt_image_is_hdr(img)) // image is 32 bit float, most likely // linear space, best guess is Rec709 d->type = DT_COLORSPACE_LIN_REC709; else // no ICC tag nor colorprofile was found - ICC spec says untagged files are sRGB d->type = DT_COLORSPACE_SRGB; dt_image_cache_write_release(img, DT_IMAGE_CACHE_RELAXED); update_profile_list(self); } static void update_profile_list(dt_iop_module_t *self) { dt_iop_colorin_gui_data_t *g = self->gui_data; if(!g) return; // clear and refill the image profile list g_list_free_full(g->image_profiles, free); g->image_profiles = NULL; g->n_image_profiles = 0; int pos = -1; // some file formats like jpeg can have an embedded color profile // currently we only support jpeg, j2k, tiff and png const dt_image_t *cimg = dt_image_cache_get(self->dev->image_storage.id, 'r'); const gboolean has_dng_look = cimg && cimg->profile_hsm_data; if(cimg && cimg->profile) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_EMBEDDED_ICC, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_EMBEDDED_ICC; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; } dt_image_cache_read_release(cimg); // use the matrix embedded in some DNGs and EXRs if(dt_is_valid_colormatrix(self->dev->image_storage.d65_color_matrix[0])) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_EMBEDDED_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_EMBEDDED_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; if(!dt_is_valid_colormatrix(self->dev->image_storage.dng_forward_matrix[0]) && has_dng_look) { prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_DNG_LOOK, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_DNG_LOOK; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; } } // use the DNG forward matrix if present -- gives the "as intended by // the DNG converter" look rather than the purely colorimetric one if(dt_is_valid_colormatrix(self->dev->image_storage.dng_forward_matrix[0])) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_FORWARD_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_FORWARD_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; } if(dt_is_valid_colormatrix(self->dev->image_storage.adobe_XYZ_to_CAM[0][0]) && !(self->dev->image_storage.flags & DT_IMAGE_4BAYER)) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_STANDARD_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_STANDARD_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; } // darktable built-in, if applicable for(int k = 0; k < dt_profiled_colormatrix_cnt; k++) { if(!strcasecmp(self->dev->image_storage.camera_makermodel, dt_profiled_colormatrices[k].makermodel)) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_ENHANCED_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_ENHANCED_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; break; } } // darktable vendor matrix, if applicable for(int k = 0; k < dt_vendor_colormatrix_cnt; k++) { if(!strcmp(self->dev->image_storage.camera_makermodel, dt_vendor_colormatrices[k].makermodel)) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_VENDOR_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_VENDOR_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; break; } } // darktable alternate matrix, if applicable for(int k = 0; k < dt_alternate_colormatrix_cnt; k++) { if(!strcmp(self->dev->image_storage.camera_makermodel, dt_alternate_colormatrices[k].makermodel)) { dt_colorspaces_color_profile_t *prof = calloc(1, sizeof(dt_colorspaces_color_profile_t)); g_strlcpy(prof->name, dt_colorspaces_get_name(DT_COLORSPACE_ALTERNATE_MATRIX, ""), sizeof(prof->name)); prof->type = DT_COLORSPACE_ALTERNATE_MATRIX; g->image_profiles = g_list_append(g->image_profiles, prof); prof->in_pos = ++pos; break; } } g->n_image_profiles = pos + 1; // update the gui dt_bauhaus_combobox_clear(g->profile_combobox); for(GList *l = g->image_profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = l->data; dt_bauhaus_combobox_add(g->profile_combobox, prof->name); } for(GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = l->data; if(prof->in_pos > -1) dt_bauhaus_combobox_add(g->profile_combobox, prof->name); } // working profile dt_bauhaus_combobox_clear(g->work_combobox); for(GList *l = darktable.color_profiles->profiles; l; l = g_list_next(l)) { dt_colorspaces_color_profile_t *prof = l->data; if(prof->work_pos > -1) dt_bauhaus_combobox_add(g->work_combobox, prof->name); } } static void _preview_pipe_finished_callback(gpointer instance, dt_iop_module_t *self) { if(!self->gui_data) return; dt_iop_colorin_params_t *p = self->params; const gboolean on = p->blue_mapping; dt_iop_set_module_trouble_message(self, on ? _("blue mapping") : NULL, on ? _("the blue mapping mode has been deprecated since very long and has been removed.\n" "reset to defaults or switch to any colorin profile and check results. minimal\n" "visual differences might be possible.") : NULL, on ? "blue mode detected in colorin module" : NULL); } void gui_init(dt_iop_module_t *self) { // pthread_mutex_lock(&darktable.plugin_threadsafe); dt_iop_colorin_gui_data_t *g = IOP_GUI_ALLOC(colorin); g->image_profiles = NULL; g->profile_combobox = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->profile_combobox, NULL, N_("input profile")); g->work_combobox = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->work_combobox, NULL, N_("working profile")); self->widget = dt_gui_vbox(g->profile_combobox, g->work_combobox); dt_bauhaus_combobox_set(g->profile_combobox, 0); // We do not set the tooltip for the input profile widget because // this tooltip will always be overwritten in reload_defaults(). dt_bauhaus_combobox_set(g->work_combobox, 0); { char *tooltip = dt_ioppr_get_location_tooltip("out", _("working ICC profiles")); gtk_widget_set_tooltip_markup(g->work_combobox, tooltip); g_free(tooltip); } g_signal_connect(G_OBJECT(g->profile_combobox), "value-changed", G_CALLBACK(_profile_changed), (gpointer)self); g_signal_connect(G_OBJECT(g->work_combobox), "value-changed", G_CALLBACK(_workicc_changed), (gpointer)self); g->clipping_combobox = dt_bauhaus_combobox_from_params(self, "normalize"); gtk_widget_set_tooltip_text(g->clipping_combobox, _("confine Lab values to gamut of RGB color space")); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _preview_pipe_finished_callback); } void gui_cleanup(dt_iop_module_t *self) { dt_iop_colorin_gui_data_t *g = self->gui_data; while(g->image_profiles) { g_free(g->image_profiles->data); g->image_profiles = g_list_delete_link(g->image_profiles, g->image_profiles); } } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on