diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d19d3229b14..38821bafdb0 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -231,6 +231,11 @@ changes (where available). colorin module, selectable manually. The existing "embedded matrix" profile remains the default for fresh imports and reloading defaults. + DNG files now also supports provided "ProfileHueSatMapData" + and applies these automatically if "DNG forward matrix" is + selected as the colorin profile. + Some images have "ProfileHueSatMapData" but no forward matrix. + You may chose the "DNG embedded look" to apply the DNG look. - The highlights module will be disabled automatically when a raw image with a clean history is opended in darkroom the first time diff --git a/src/common/colorspaces.c b/src/common/colorspaces.c index 21200bf426d..adc39e8f417 100644 --- a/src/common/colorspaces.c +++ b/src/common/colorspaces.c @@ -1836,6 +1836,8 @@ const char *dt_colorspaces_get_name(dt_colorspaces_color_profile_type_t type, return _("Display P3"); case DT_COLORSPACE_FORWARD_MATRIX: return _("DNG forward matrix"); + case DT_COLORSPACE_DNG_LOOK: + return _("DNG embedded look"); case DT_COLORSPACE_LAST: break; } @@ -2654,6 +2656,7 @@ gboolean dt_colorspaces_profile_is_wide_gamut(const dt_colorspaces_color_profile case DT_COLORSPACE_DISPLAY2: case DT_COLORSPACE_EMBEDDED_ICC: case DT_COLORSPACE_EMBEDDED_MATRIX: + case DT_COLORSPACE_DNG_LOOK: case DT_COLORSPACE_STANDARD_MATRIX: case DT_COLORSPACE_ENHANCED_MATRIX: case DT_COLORSPACE_VENDOR_MATRIX: diff --git a/src/common/colorspaces.h b/src/common/colorspaces.h index 19110ab7b62..702c8437df6 100644 --- a/src/common/colorspaces.h +++ b/src/common/colorspaces.h @@ -105,7 +105,8 @@ typedef enum dt_colorspaces_color_profile_type_t DT_COLORSPACE_HLG_P3 = 25, DT_COLORSPACE_DISPLAY_P3 = 26, DT_COLORSPACE_FORWARD_MATRIX = 27, - DT_COLORSPACE_LAST = 28 + DT_COLORSPACE_DNG_LOOK = 28, + DT_COLORSPACE_LAST = 29 } dt_colorspaces_color_profile_type_t; typedef enum dt_colorspaces_color_mode_t diff --git a/src/common/exif.cc b/src/common/exif.cc index 62dd192e73a..be4cda21d99 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -1077,8 +1077,8 @@ static bool _valid_dng_matrix(const float *M) return fabsf(det) > 1e-6f; } -static void _check_forward_matrix(Exiv2::ExifData &exifData, - dt_image_t *img) +static int _check_forward_matrix(Exiv2::ExifData &exifData, + dt_image_t *img) { Exiv2::ExifData::const_iterator pos; dt_mark_colormatrix_invalid(&img->dng_forward_matrix[0]); @@ -1202,7 +1202,7 @@ static void _check_forward_matrix(Exiv2::ExifData &exifData, // We only want a valid forward matrix if we got an illu and matrix are good const gboolean has_forward_illuminant = sel_illu >= 0 && sel_illu < 3 && has_FM[sel_illu]; if(!has_forward_illuminant) - return; + return sel_illu; // CameraToXYZ = ForwardMatrix * Inverse(AnalogBalance * CameraCalibration) // (white balance D is applied later, upstream, via temperature.iop's wb_coeffs) @@ -1226,7 +1226,52 @@ static void _check_forward_matrix(Exiv2::ExifData &exifData, illu[2] ? ", [3] " : "", illu[2] ? _illu_to_str(illu[2]) : "" ); _print_matrix_data("img forward matrix", 0, img->dng_forward_matrix); _print_matrix_data(has_CC[sel_illu] ? "calibration matrix" : "ident calibration matrix", 0, CC[sel_illu]); + return sel_illu; + } + return sel_illu; +} + +static void _check_profile_look_table(Exiv2::ExifData &exifData, + dt_image_t *img, + const int sel_illu) +{ + g_free(img->profile_hsm_data); + img->profile_hsm_data = NULL; + img->profile_hsm_hue_div = img->profile_hsm_sat_div = img->profile_hsm_val_div = 0; + Exiv2::ExifData::const_iterator dims_pos = + exifData.findKey(Exiv2::ExifKey("Exif.Image.ProfileHueSatMapDims")); + if(dims_pos != exifData.end() && dims_pos->count() == 3) + { + const auto hue_div = dims_pos->toLong(0); + const auto sat_div = dims_pos->toLong(1); + const auto val_div = dims_pos->toLong(2); + if(hue_div >= 1 && sat_div >= 2 && val_div >= 1 + && hue_div <= G_MAXINT / 3 / sat_div / val_div) + { + const int n_entries = hue_div * sat_div * val_div * 3; + const char *keys[] = { + "Exif.Image.ProfileHueSatMapData1", + "Exif.Image.ProfileHueSatMapData2", +#if EXIV2_TEST_VERSION(0,27,4) + "Exif.Image.ProfileHueSatMapData3" +#endif + }; + const int index = sel_illu >= 0 && sel_illu < (int)G_N_ELEMENTS(keys) ? sel_illu : 0; + Exiv2::ExifData::const_iterator pos = exifData.findKey(Exiv2::ExifKey(keys[index])); + if(pos != exifData.end() && (int)pos->count() == n_entries) + { + img->profile_hsm_data = (float *)g_malloc_n(n_entries, sizeof(float)); + for(int i = 0; i < n_entries; i++) + img->profile_hsm_data[i] = pos->toFloat(i); + img->profile_hsm_hue_div = hue_div; + img->profile_hsm_sat_div = sat_div; + img->profile_hsm_val_div = val_div; + dt_print(DT_DEBUG_IMAGEIO, "[exif] found ProfileHueSatMapData%d (%dx%dx%d)", + index + 1, img->profile_hsm_hue_div, img->profile_hsm_sat_div, img->profile_hsm_val_div); + } + } } + } static gboolean _check_dng_opcodes(Exiv2::ExifData &exifData, @@ -1682,7 +1727,8 @@ void dt_exif_img_check_additional_tags(dt_image_t *img, _check_dng_opcodes(exifData, img); _check_lens_correction_data(exifData, img); _check_linear_response_limit(exifData, img); - _check_forward_matrix(exifData, img); + const int sel_illu = _check_forward_matrix(exifData, img); + _check_profile_look_table(exifData, img, sel_illu); _check_highlight_preservation(exifData, img); _check_shading_compensation(exifData, img); } @@ -3298,7 +3344,6 @@ int dt_exif_read_blob(uint8_t **buf, "Exif.Image.ProfileHueSatMapDims", "Exif.Image.ProfileHueSatMapData1", "Exif.Image.ProfileHueSatMapData2", - "Exif.Image.ProfileToneCurve", "Exif.Image.ProfileEmbedPolicy", "Exif.Image.ProfileCopyright", "Exif.Image.ForwardMatrix1", diff --git a/src/common/image.c b/src/common/image.c index a75c8970ab4..eedd3ef9c9e 100644 --- a/src/common/image.c +++ b/src/common/image.c @@ -2245,6 +2245,8 @@ void dt_image_init(dt_image_t *img) img->raw_white_point = 16384; // 2^14 dt_mark_colormatrix_invalid(&img->d65_color_matrix[0]); dt_mark_colormatrix_invalid(&img->dng_forward_matrix[0]); + img->profile_hsm_data = NULL; + img->profile_hsm_hue_div = img->profile_hsm_sat_div = img->profile_hsm_val_div = 0; img->profile = NULL; img->profile_size = 0; img->colorspace = DT_IMAGE_COLORSPACE_NONE; diff --git a/src/common/image.h b/src/common/image.h index ff7d865c239..869574a0f4d 100644 --- a/src/common/image.h +++ b/src/common/image.h @@ -324,6 +324,10 @@ typedef struct dt_image_t float d65_color_matrix[9]; // the 3x3 matrix embedded in some DNGs float dng_forward_matrix[9]; // the 3x3 forward matrix supplied in some DNG files + float *profile_hsm_data; // hue_div * sat_div * val_div * 3 floats, owned by the struct + int profile_hsm_hue_div; + int profile_hsm_sat_div; + int profile_hsm_val_div; uint8_t *profile; // embedded profile, for example from JPEGs uint32_t profile_size; dt_image_colorspace_t colorspace; // the colorspace that is diff --git a/src/common/image_cache.c b/src/common/image_cache.c index 2ef90ba0114..8d08d571ef9 100644 --- a/src/common/image_cache.c +++ b/src/common/image_cache.c @@ -116,6 +116,9 @@ static void _image_cache_allocate(void *data, g_free(img->profile); img->profile = NULL; img->profile_size = 0; + g_free(img->profile_hsm_data); + img->profile_hsm_data = NULL; + img->profile_hsm_hue_div = img->profile_hsm_sat_div = img->profile_hsm_val_div = 0; img->colorspace = sqlite3_column_int(stmt, 23); img->version = sqlite3_column_int(stmt, 24); img->raw_black_level = sqlite3_column_int(stmt, 25); @@ -205,6 +208,7 @@ static void _image_cache_deallocate(void *data, dt_cache_entry_t *entry) { dt_image_t *img = entry->data; g_free(img->profile); + g_free(img->profile_hsm_data); g_list_free_full(img->dng_gain_maps, g_free); g_free(img); entry->data = NULL; diff --git a/src/common/iop_order.c b/src/common/iop_order.c index 58bc689b40e..758f4649a0d 100644 --- a/src/common/iop_order.c +++ b/src/common/iop_order.c @@ -113,6 +113,7 @@ const dt_iop_order_entry_t legacy_order[] = { { {25.0f }, "profile_gamma", 0}, { {26.0f }, "hazeremoval", 0}, { {27.0f }, "colorin", 0}, + { {27.1f }, "dng_look", 0}, { {27.5f }, "channelmixerrgb", 0}, { {27.5f }, "contrastntexture", 0}, { {27.5f }, "diffuse", 0}, @@ -221,6 +222,7 @@ const dt_iop_order_entry_t v30_order[] = { { {26.0f }, "profile_gamma", 0}, { {27.0f }, "equalizer", 0}, { {28.0f }, "colorin", 0}, + { {28.1f }, "dng_look", 0}, { {28.5f }, "channelmixerrgb", 0}, { {28.5f }, "contrastntexture", 0}, { {28.5f }, "diffuse", 0}, @@ -344,6 +346,7 @@ const dt_iop_order_entry_t v50_order[] = { { {26.0f }, "profile_gamma", 0}, { {27.0f }, "equalizer", 0}, { {28.0f }, "colorin", 0}, + { {28.1f }, "dng_look", 0}, { {28.5f }, "channelmixerrgb", 0}, { {28.5f }, "contrastntexture", 0}, { {28.5f }, "diffuse", 0}, @@ -441,6 +444,7 @@ const dt_iop_order_entry_t v30_jpg_order[] = { { { 8.5f }, "demosaicscale", 0 }, // all the modules between [8.5; 28] expect linear RGB, so they need to be moved after colorin { { 28.0f }, "colorin", 0 }, + { { 28.0f }, "dng_look", 0 }, // moved modules : (copy-pasted in the same order) { { 28.0f }, "denoiseprofile", 0}, { { 28.0f }, "bilateral", 0}, @@ -567,6 +571,7 @@ const dt_iop_order_entry_t v50_jpg_order[] = { { { 8.5f }, "demosaicscale", 0 }, // all the modules between [8.5; 28] expect linear RGB, so they need to be moved after colorin { { 28.0f }, "colorin", 0 }, + { { 28.0f }, "dng_look", 0 }, // moved modules : (copy-pasted in the same order) { { 28.0f }, "denoiseprofile", 0}, { { 28.0f }, "bilateral", 0}, @@ -767,6 +772,7 @@ void dt_ioppr_migrate_legacy_iop_order_list(GList *iop_order_list) _insert_before_after(iop_order_list, "highlights", "rasterfile", TRUE); _insert_before_after(iop_order_list, "demosaic", "demosaicscale", FALSE); _insert_before_after(iop_order_list, "colorbalance", "colorharmonizer", TRUE); + _insert_before_after(iop_order_list, "colorin", "dng_look", FALSE); } static dt_iop_order_t _ioppr_get_default_iop_order_version(const dt_imgid_t imgid) diff --git a/src/common/iop_profile.c b/src/common/iop_profile.c index 76ee7d56a5b..d766a62bea4 100644 --- a/src/common/iop_profile.c +++ b/src/common/iop_profile.c @@ -937,8 +937,10 @@ dt_ioppr_set_pipe_input_profile_info(struct dt_develop_t *dev, dt_image_cache_write_release_info(wimg, DT_IMAGE_CACHE_RELAXED, NULL); } - if(ptype >= DT_COLORSPACE_EMBEDDED_ICC - && ptype <= DT_COLORSPACE_ALTERNATE_MATRIX) + if((ptype >= DT_COLORSPACE_EMBEDDED_ICC + && ptype <= DT_COLORSPACE_ALTERNATE_MATRIX) + || ptype == DT_COLORSPACE_FORWARD_MATRIX + || ptype == DT_COLORSPACE_DNG_LOOK) { /* We have a camera input matrix, these are not generated from files but in colorin, * so we need to fetch and replace them from somewhere. diff --git a/src/common/mipmap_cache.c b/src/common/mipmap_cache.c index 0f37ee79a00..815bf4e2854 100644 --- a/src/common/mipmap_cache.c +++ b/src/common/mipmap_cache.c @@ -986,6 +986,8 @@ void dt_mipmap_cache_get_with_caller(dt_mipmap_buffer_t *buf, dt_image_t DT_ALIGNED_ARRAY buffered_image; const dt_image_t *cimg = dt_image_cache_get(imgid, 'r'); buffered_image = *cimg; + buffered_image.profile_hsm_data = NULL; + buffered_image.profile_hsm_hue_div = buffered_image.profile_hsm_sat_div = buffered_image.profile_hsm_val_div = 0; // dt_image_t *img = dt_image_cache_write_get(cimg); // dt_image_cache_write_release(img, DT_IMAGE_CACHE_RELAXED); dt_image_cache_read_release(cimg); @@ -1010,6 +1012,7 @@ void dt_mipmap_cache_get_with_caller(dt_mipmap_buffer_t *buf, { // swap back new image data: dt_image_t *img = dt_image_cache_get(imgid, 'w'); + g_free(img->profile_hsm_data); *img = buffered_image; img->load_status = DT_IMAGEIO_OK; // dt_print(DT_DEBUG_ALWAYS, "[mipmap read get] initializing full buffer img %u with %u %u -> %d %d (%p)", @@ -1019,6 +1022,7 @@ void dt_mipmap_cache_get_with_caller(dt_mipmap_buffer_t *buf, } else { + g_free(buffered_image.profile_hsm_data); dt_print(DT_DEBUG_PIPE, "[mipmap read get] error loading ID=%d", imgid); // // we can only return a zero dimension buffer if the buffer diff --git a/src/iop/CMakeLists.txt b/src/iop/CMakeLists.txt index baecf677763..ed387089a96 100644 --- a/src/iop/CMakeLists.txt +++ b/src/iop/CMakeLists.txt @@ -82,6 +82,7 @@ add_iop(equalizer "equalizer.c") add_iop(rgbcurve "rgbcurve.c") add_iop(colorbalance "colorbalance.c" DEFAULT_VISIBLE) add_iop(colorin "colorin.c" DEFAULT_VISIBLE) +add_iop(dng_look "dng_look.c") add_iop(colorout "colorout.c") add_iop(colorchecker "colorchecker.c") add_iop(clipping "clipping.c" DEFAULT_VISIBLE) diff --git a/src/iop/colorin.c b/src/iop/colorin.c index 856169e8877..e11c5638b4e 100644 --- a/src/iop/colorin.c +++ b/src/iop/colorin.c @@ -522,9 +522,16 @@ static void _profile_changed(GtkWidget *widget, dt_iop_module_t *self) 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); @@ -1384,7 +1391,7 @@ void commit_params(dt_iop_module_t *self, else type = DT_COLORSPACE_EMBEDDED_MATRIX; } - if(type == DT_COLORSPACE_EMBEDDED_MATRIX) + if(type == DT_COLORSPACE_EMBEDDED_MATRIX || type == DT_COLORSPACE_DNG_LOOK) { // embedded matrix, hopefully D65 const dt_image_t *cimg = dt_image_cache_get(pipe->image.id, 'r'); @@ -1964,6 +1971,7 @@ static void update_profile_list(dt_iop_module_t *self) // 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)); @@ -1983,6 +1991,16 @@ static void update_profile_list(dt_iop_module_t *self) 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 diff --git a/src/iop/dng_look.c b/src/iop/dng_look.c new file mode 100644 index 00000000000..009acced102 --- /dev/null +++ b/src/iop/dng_look.c @@ -0,0 +1,277 @@ +/* + This file is part of darktable, + Copyright (C) 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 "common/colorspaces_inline_conversions.h" +#include "common/image_cache.h" +#include "common/imagebuf.h" +#include "common/iop_profile.h" +#include "develop/develop.h" +#include "develop/imageop.h" +#include "gui/gtk.h" +#include "iop/iop_api.h" + +DT_MODULE_INTROSPECTION(2, dt_iop_dng_look_params_t) + +typedef struct dt_iop_dng_look_params_t +{ + int reserved; // $DEFAULT: 0 +} dt_iop_dng_look_params_t; + +typedef struct dt_iop_dng_look_data_t +{ + float *hsm; + int hue_div, sat_div, val_div; +} dt_iop_dng_look_data_t; + +const char *name() +{ + return _("dng look"); +} + +int flags() +{ + return IOP_FLAGS_HIDDEN | IOP_FLAGS_ONE_INSTANCE | IOP_FLAGS_ALLOW_TILING; +} + +int default_group() +{ + return IOP_GROUP_COLOR | IOP_GROUP_TECHNICAL; +} + +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; +} + +void reload_defaults(dt_iop_module_t *self) +{ + // defaults load before colorin and this image's history (develop.c:2458) + // stay off here; enable the hidden pipe piece in commit_params once history is available + self->default_enabled = FALSE; +} + +static gboolean _dng_look_profile_selected(const dt_develop_t *dev) +{ + const dt_iop_module_t *colorin = dt_iop_get_module_from_list(dev->iop, "colorin"); + if(!colorin || !colorin->get_p || !colorin->default_params + || g_list_find_custom(dev->module_filter_out, "colorin", (GCompareFunc)g_strcmp0)) + return FALSE; + + const dt_iop_params_t *params = colorin->default_params; + gboolean enabled = colorin->default_enabled; + const GList *history = dev->history; + for(int i = 0; i < dev->history_end && history; i++, history = history->next) + { + const dt_dev_history_item_t *hist = history->data; + if(hist->module == colorin) + { + params = hist->params; + enabled = hist->enabled; + } + } + + const dt_colorspaces_color_profile_type_t *type = + params ? colorin->get_p(params, "type") : NULL; + return enabled && type + && ((*type == DT_COLORSPACE_FORWARD_MATRIX + && dt_is_valid_colormatrix(dev->image_storage.dng_forward_matrix[0])) + || *type == DT_COLORSPACE_DNG_LOOK); +} + +static void _lookup_hsm(const dt_iop_dng_look_data_t *d, + const dt_aligned_pixel_t hsv, + dt_aligned_pixel_t correction) +{ + const float h = (hsv[0] - floorf(hsv[0])) * d->hue_div; + const float s = CLIP(hsv[1]) * (d->sat_div - 1); + const float v = CLIP(hsv[2]) * (d->val_div - 1); + const int hi[2] = { MIN((int)h, d->hue_div - 1), + (MIN((int)h, d->hue_div - 1) + 1) % d->hue_div }; + const int si[2] = { MIN((int)s, d->sat_div - 1), + MIN((int)s + 1, d->sat_div - 1) }; + const int vi[2] = { MIN((int)v, d->val_div - 1), + MIN((int)v + 1, d->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 fastest, then hue, then value, with three floats per cell + const size_t index = 3 * (((size_t)vi[z] * d->hue_div + hi[y]) * d->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 * d->hsm[index + c]; + } +} + +void commit_params(dt_iop_module_t *self, + dt_iop_params_t *params, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + dt_iop_dng_look_data_t *d = piece->data; + g_free(d->hsm); + d->hsm = NULL; + d->hue_div = d->sat_div = d->val_div = 0; + piece->process_cl_ready = FALSE; + + // image_storage borrows cache-owned tables; keep them locked until the pipe has its own copies + const dt_imgid_t imgid = self->dev->image_storage.id; + const gboolean cached = dt_is_valid_imgid(imgid); + const dt_image_t *img = cached ? dt_image_cache_get(imgid, 'r') : &self->dev->image_storage; + if(!img) + { + piece->enabled = FALSE; + return; + } + + // require a look-capable profile selection, not just available metadata + // pipe sync holds history_mutex, and the last active history entry wins regardless of commit order + const gboolean enabled = + _dng_look_profile_selected(self->dev) + && img->profile_hsm_data != NULL + && !g_list_find_custom(self->dev->module_filter_out, self->op, (GCompareFunc)g_strcmp0); + // auto-enable defaults, but preserve an explicit disable recorded in history + piece->enabled = enabled && (params == self->default_params || piece->enabled); + + if(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 size_t count = (size_t)img->profile_hsm_hue_div * img->profile_hsm_sat_div + * img->profile_hsm_val_div * 3; + gboolean valid = TRUE; + for(size_t 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_try_malloc_n(count, sizeof(float)); + if(d->hsm) + { + memcpy(d->hsm, img->profile_hsm_data, 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; + } + } + } + if(cached) + dt_image_cache_read_release(img); +} + +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, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) + return; + + const dt_iop_dng_look_data_t *d = piece->data; + // synch_top can change only colorin: never apply a stale look after switching away + // colorin requests full sync on look-profile transitions to re-evaluate enablement + const dt_iop_order_iccprofile_info_t *input_profile = + dt_ioppr_get_pipe_input_profile_info(piece->pipe); + + // the DNG hue/saturation tables are specified for ProPhoto primaries! + const dt_iop_order_iccprofile_info_t *work_profile = + dt_ioppr_get_pipe_work_profile_info(piece->pipe); + + if(!input_profile + || !work_profile + || (input_profile->type != DT_COLORSPACE_FORWARD_MATRIX + && input_profile->type != DT_COLORSPACE_DNG_LOOK) + || !d->hsm) + { + dt_iop_image_copy(ovoid, ivoid, (size_t)4 * roi_out->width * roi_out->height); + return; + } + + DT_OMP_FOR(collapse(2)) + for(size_t row = 0; row < roi_out->height; row++) + { + for(size_t col = 0; col < roi_out->width; col++) + { + const float *in = (const float *)ivoid + (size_t)4 * (roi_in->width * row + col); + float *out = (float *)ovoid + (size_t)4 * (roi_out->width * row + col); + dt_aligned_pixel_t rgb = { in[0], in[1], in[2], in[3] }; + dt_aligned_pixel_t look_rgb; + dt_aligned_pixel_t XYZ; + dt_apply_transposed_color_matrix(rgb, work_profile->matrix_in_transposed, XYZ); + dt_XYZ_to_prophotorgb(XYZ, look_rgb); + + dt_aligned_pixel_t hsv; + dt_aligned_pixel_t correction; + dt_RGB_2_HSV(look_rgb, hsv); + _lookup_hsm(d, hsv, correction); + hsv[0] += correction[0] / 360.0f; + hsv[0] -= floorf(hsv[0]); + hsv[1] = CLIP(hsv[1] * correction[1]); + dt_HSV_2_RGB(hsv, look_rgb); + + // dt_aligned_pixel_t XYZ; + dt_prophotorgb_to_XYZ(look_rgb, XYZ); + dt_apply_transposed_color_matrix(XYZ, work_profile->matrix_out_transposed, rgb); + rgb[3] = in[3]; + copy_pixel(out, rgb); + } + } +} + +void init_pipe(dt_iop_module_t *self, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + piece->data = calloc(1, sizeof(dt_iop_dng_look_data_t)); +} + +void cleanup_pipe(dt_iop_module_t *self, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + dt_iop_dng_look_data_t *d = piece->data; + g_free(d->hsm); + free(d); + piece->data = NULL; +} + +void gui_init(dt_iop_module_t *self) +{ + self->widget = dt_gui_vbox(dt_ui_label_new(_("automatically derived from the embedded DNG profile"))); +} +// 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 diff --git a/src/tests/unittests/iop/CMakeLists.txt b/src/tests/unittests/iop/CMakeLists.txt index 094a0924ae6..46e84241957 100644 --- a/src/tests/unittests/iop/CMakeLists.txt +++ b/src/tests/unittests/iop/CMakeLists.txt @@ -6,8 +6,18 @@ add_cmocka_test(test_lut3d_bounds SOURCES test_lut3d_bounds.c LINK_LIBRARIES lib_darktable cmocka) +add_cmocka_test(test_dng_look + SOURCES test_dng_look.c + LINK_LIBRARIES lib_darktable cmocka) + +add_cmocka_test(test_colorin + SOURCES test_colorin.c + LINK_LIBRARIES lib_darktable cmocka) + # Windows: libs have to be copied next to the executable if(WIN32) _copy_required_library(test_filmicrgb lib_darktable) _copy_required_library(test_lut3d_bounds lib_darktable) + _copy_required_library(test_dng_look lib_darktable) + _copy_required_library(test_colorin lib_darktable) endif(WIN32) diff --git a/src/tests/unittests/iop/test_colorin.c b/src/tests/unittests/iop/test_colorin.c new file mode 100644 index 00000000000..21fc07c30fe --- /dev/null +++ b/src/tests/unittests/iop/test_colorin.c @@ -0,0 +1,238 @@ +/* + This file is part of darktable, + Copyright (C) 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 +#include +#include +#include + +#define dt_image_cache_get __wrap_dt_image_cache_get +#define dt_image_cache_read_release __wrap_dt_image_cache_read_release +#define dt_bauhaus_combobox_clear __wrap_dt_bauhaus_combobox_clear +#define dt_bauhaus_combobox_add __wrap_dt_bauhaus_combobox_add +#define dt_bauhaus_combobox_get __wrap_dt_bauhaus_combobox_get +#define dt_iop_request_focus __wrap_dt_iop_request_focus +#define dt_dev_add_history_item __wrap_dt_dev_add_history_item +#define dt_control_signal_raise __wrap_dt_control_signal_raise + +#include "common/iop_profile.c" +#include "iop/colorin.c" + +#ifdef _WIN32 +#include "win/main_wrapper.h" +#endif + +static dt_image_t image; +static gboolean cache_locked; +static int selected_profile; + +dt_image_t *__wrap_dt_image_cache_get(const dt_imgid_t imgid, const char mode) +{ + assert_int_equal(imgid, image.id); + assert_int_equal(mode, 'r'); + assert_false(cache_locked); + cache_locked = TRUE; + return ℑ +} + +void __wrap_dt_image_cache_read_release(const dt_image_t *img) +{ + assert_ptr_equal(img, &image); + assert_true(cache_locked); + cache_locked = FALSE; +} + +void __wrap_dt_bauhaus_combobox_clear(GtkWidget *widget) {} +void __wrap_dt_bauhaus_combobox_add(GtkWidget *widget, const char *text) {} +int __wrap_dt_bauhaus_combobox_get(GtkWidget *widget) { return selected_profile; } +void __wrap_dt_iop_request_focus(dt_iop_module_t *module) {} +void __wrap_dt_control_signal_raise(const struct dt_control_signal_t *signals, + const dt_signal_t signal, ...) {} + +void __wrap_dt_dev_add_history_item(dt_develop_t *dev, dt_iop_module_t *module, + const gboolean enable) +{ + assert_ptr_equal(dev, module->dev); + assert_true(enable); + dev->full.pipe->changed |= DT_DEV_PIPE_TOP_CHANGED; + dev->preview_pipe->changed |= DT_DEV_PIPE_TOP_CHANGED; + dev->preview2.pipe->changed |= DT_DEV_PIPE_TOP_CHANGED; +} + +static void test_dng_look_profile_list(void **state) +{ + dt_colorspaces_t profiles = { 0 }; + darktable.color_profiles = &profiles; + dt_develop_t dev = { 0 }; + dt_iop_colorin_gui_data_t gui = { 0 }; + dt_iop_module_t module = { .dev = &dev, .gui_data = &gui }; + float table[] = { 0.0f, 1.0f, 1.0f }; + for(int d65 = 0; d65 < 2; d65++) + for(int forward = 0; forward < 2; forward++) + for(int data = 0; data < 2; data++) + { + dt_mark_colormatrix_invalid(&dev.image_storage.d65_color_matrix[0]); + dt_mark_colormatrix_invalid(&dev.image_storage.dng_forward_matrix[0]); + if(d65) dev.image_storage.d65_color_matrix[0] = 1.0f; + if(forward) dev.image_storage.dng_forward_matrix[0] = 1.0f; + image.profile_hsm_data = data & 1 ? table : NULL; + update_profile_list(&module); + assert_false(cache_locked); + int found = 0; + int pos = 0; + for(const GList *l = gui.image_profiles; l; l = l->next) + { + const dt_colorspaces_color_profile_t *profile = l->data; + assert_int_equal(profile->in_pos, pos++); + if(profile->type == DT_COLORSPACE_DNG_LOOK) found++; + } + assert_int_equal(found, d65 && !forward && data != 0); + assert_int_equal(gui.n_image_profiles, pos); + } + g_list_free_full(gui.image_profiles, free); + darktable.color_profiles = NULL; +} + +static void test_embedded_matrix_commit(void **state) +{ + const float camera_matrix[9] = + { 3.2404542f, -1.5371385f, -0.4985314f, + -0.9692660f, 1.8760108f, 0.0415560f, + 0.0556434f, -0.2040259f, 1.0572252f }; + cmsCIExyY whitepoint; + cmsWhitePointFromTemp(&whitepoint, 6504.0); + const cmsCIExyYTRIPLE primaries = { { 0.64, 0.33, 1.0 }, + { 0.30, 0.60, 1.0 }, + { 0.15, 0.06, 1.0 } }; + cmsToneCurve *gamma = cmsBuildGamma(NULL, 1.0); + cmsToneCurve *curves[] = { gamma, gamma, gamma }; + dt_colorspaces_color_profile_t lab = { .type = DT_COLORSPACE_LAB, + .profile = cmsCreateLab4Profile(NULL) }; + dt_colorspaces_color_profile_t rec709 = { .type = DT_COLORSPACE_LIN_REC709, + .profile = cmsCreateRGBProfile(&whitepoint, &primaries, curves) }; + cmsFreeToneCurve(gamma); + dt_colorspaces_t profiles = { 0 }; + profiles.profiles = g_list_append(NULL, &lab); + profiles.profiles = g_list_append(profiles.profiles, &rec709); + darktable.color_profiles = &profiles; + dt_develop_t dev = { 0 }; + dt_iop_module_t module = { .dev = &dev }; + dt_dev_pixelpipe_t pipe = { 0 }; + dt_dev_pixelpipe_iop_t piece = { .pipe = &pipe }; + dt_iop_colorin_params_t params = { .type_work = DT_COLORSPACE_LIN_REC709 }; + init_pipe(&module, &pipe, &piece); + dt_iop_colorin_data_t *d = piece.data; + + for(int fallback = 0; fallback < 3; fallback++) + { + memcpy(image.d65_color_matrix, camera_matrix, sizeof(camera_matrix)); + memset(pipe.image.adobe_XYZ_to_CAM, 0, sizeof(pipe.image.adobe_XYZ_to_CAM)); + if(fallback) dt_mark_colormatrix_invalid(&image.d65_color_matrix[0]); + if(fallback == 1) + memcpy(pipe.image.adobe_XYZ_to_CAM, camera_matrix, sizeof(camera_matrix)); + params.type = DT_COLORSPACE_EMBEDDED_MATRIX; + commit_params(&module, (dt_iop_params_t *)¶ms, &pipe, &piece); + assert_true(dt_is_valid_colormatrix(d->cmatrix[0][0])); + dt_colormatrix_t expected; + memcpy(expected, d->cmatrix, sizeof(expected)); + params.type = DT_COLORSPACE_DNG_LOOK; + commit_params(&module, (dt_iop_params_t *)¶ms, &pipe, &piece); + assert_int_equal(d->type, DT_COLORSPACE_DNG_LOOK); + assert_int_equal(d->clear_input, fallback < 2); + assert_int_equal(pipe.input_profile_info->type, DT_COLORSPACE_DNG_LOOK); + assert_memory_equal(d->cmatrix, expected, sizeof(expected)); + assert_memory_equal(pipe.input_profile_info->matrix_in, expected, sizeof(expected)); + assert_true(dt_is_valid_colormatrix(pipe.input_profile_info->matrix_out[0][0])); + assert_false(cache_locked); + } + + const float forward_matrix[9] = + { 0.4124564f, 0.3575761f, 0.1804375f, + 0.2126729f, 0.7151522f, 0.0721750f, + 0.0193339f, 0.1191920f, 0.9503041f }; + memcpy(pipe.image.dng_forward_matrix, forward_matrix, sizeof(forward_matrix)); + params.type = DT_COLORSPACE_FORWARD_MATRIX; + commit_params(&module, (dt_iop_params_t *)¶ms, &pipe, &piece); + assert_int_equal(pipe.input_profile_info->type, DT_COLORSPACE_FORWARD_MATRIX); + assert_memory_equal(pipe.input_profile_info->matrix_in, d->cmatrix, sizeof(d->cmatrix)); + assert_true(dt_is_valid_colormatrix(pipe.input_profile_info->matrix_out[0][0])); + + cleanup_pipe(&module, &pipe, &piece); + for(GList *l = dev.allprofile_info; l; l = l->next) + { + dt_ioppr_cleanup_profile_info(l->data); + dt_free_align(l->data); + } + g_list_free(dev.allprofile_info); + cmsCloseProfile(lab.profile); + cmsCloseProfile(rec709.profile); + g_list_free(profiles.profiles); + darktable.color_profiles = NULL; +} + +static void test_profile_switch_resync(void **state) +{ + dt_gui_gtk_t gui = { 0 }; + darktable.gui = &gui; + dt_dev_pixelpipe_t pipes[3] = { 0 }; + dt_develop_t dev = { .preview_pipe = &pipes[1] }; + dev.full.pipe = &pipes[0]; + dev.preview2.pipe = &pipes[2]; + darktable.develop = &dev; + dt_iop_colorin_params_t params = { 0 }; + dt_colorspaces_color_profile_t profile = { .in_pos = 0 }; + dt_iop_colorin_gui_data_t data = { .n_image_profiles = 1 }; + data.image_profiles = g_list_append(NULL, &profile); + dt_iop_module_t module = { .dev = &dev, .params = ¶ms, .gui_data = &data }; + const dt_colorspaces_color_profile_type_t types[] = + { DT_COLORSPACE_FORWARD_MATRIX, DT_COLORSPACE_DNG_LOOK, + DT_COLORSPACE_EMBEDDED_MATRIX, DT_COLORSPACE_LIN_REC709 }; + for(int old = 0; old < (int)G_N_ELEMENTS(types); old++) + for(int next = 0; next < (int)G_N_ELEMENTS(types); next++) + { + params.type = types[old]; + profile.type = types[next]; + for(int i = 0; i < 3; i++) pipes[i].changed = DT_DEV_PIPE_UNCHANGED; + _profile_changed(NULL, &module); + assert_int_equal(params.type, profile.type); + const gboolean expected = (old < 2) != (next < 2); + for(int i = 0; i < 3; i++) + { + assert_true(pipes[i].changed & DT_DEV_PIPE_TOP_CHANGED); + assert_int_equal(!!(pipes[i].changed & DT_DEV_PIPE_SYNCH), expected); + } + } + g_list_free(data.image_profiles); + darktable.gui = NULL; + darktable.develop = NULL; +} + +int main(int argc, char *argv[]) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_dng_look_profile_list), + cmocka_unit_test(test_embedded_matrix_commit), + cmocka_unit_test(test_profile_switch_resync), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} +// 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 diff --git a/src/tests/unittests/iop/test_dng_look.c b/src/tests/unittests/iop/test_dng_look.c new file mode 100644 index 00000000000..4de5a81dcc2 --- /dev/null +++ b/src/tests/unittests/iop/test_dng_look.c @@ -0,0 +1,609 @@ +/* + This file is part of darktable, + Copyright (C) 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 +#include +#include + +#include + +#define dt_image_cache_get __wrap_dt_image_cache_get +#define dt_image_cache_read_release __wrap_dt_image_cache_read_release + +#include "iop/dng_look.c" +#include "common/iop_order.h" +#include "common/matrices.h" + +#undef dt_image_cache_get +#undef dt_image_cache_read_release + +#ifdef _WIN32 +#include "win/main_wrapper.h" +#endif + +static dt_image_t *cached_image; +static gboolean cache_locked; + +dt_image_t *__wrap_dt_image_cache_get(const dt_imgid_t imgid, const char mode) +{ + assert_non_null(cached_image); + assert_int_equal(imgid, cached_image->id); + assert_int_equal(mode, 'r'); + assert_false(cache_locked); + cache_locked = TRUE; + return cached_image; +} + +void __wrap_dt_image_cache_read_release(const dt_image_t *img) +{ + assert_ptr_equal(img, cached_image); + assert_true(cache_locked); + cache_locked = FALSE; +} + +static void test_interpolation_and_hue_wrap(void **state) +{ + float table[24]; + for(int v = 0; v < 2; v++) + for(int h = 0; h < 2; h++) + for(int s = 0; s < 2; s++) + { + const int index = 3 * ((v * 2 + h) * 2 + s); + table[index] = 100.0f * v + 10.0f * h + s; + table[index + 1] = 1.0f + s; + table[index + 2] = 1.0f + v; + } + const dt_iop_dng_look_data_t d = { .hsm = table, .hue_div = 2, .sat_div = 2, .val_div = 2 }; + dt_aligned_pixel_t hsv = { 0.25f, 0.5f, 0.5f, 0.0f }; + dt_aligned_pixel_t correction; + _lookup_hsm(&d, hsv, correction); + assert_float_equal(correction[0], 55.5f, 1e-6f); + assert_float_equal(correction[1], 1.5f, 1e-6f); + assert_float_equal(correction[2], 1.5f, 1e-6f); + + hsv[0] = 0.875f; + _lookup_hsm(&d, hsv, correction); + assert_float_equal(correction[0], 53.0f, 1e-6f); + hsv[0] = -0.125f; + _lookup_hsm(&d, hsv, correction); + assert_float_equal(correction[0], 53.0f, 1e-6f); + + hsv[0] = 1.0f; + hsv[1] = 2.0f; + hsv[2] = -1.0f; + _lookup_hsm(&d, hsv, correction); + assert_float_equal(correction[0], 1.0f, 1e-6f); +} + +static void test_full_saturation_and_unchanged_value(void **state) +{ + float table[] = { 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; + dt_iop_dng_look_data_t d = { .hsm = table, .hue_div = 1, .sat_div = 2, .val_div = 1 }; + dt_iop_module_t module = { 0 }; + dt_iop_order_iccprofile_info_t profile = { .type = DT_COLORSPACE_FORWARD_MATRIX }; + dt_iop_order_iccprofile_info_t work_profile = { 0 }; + memcpy(work_profile.matrix_in_transposed, prophotorgb_to_xyz_transpose, + sizeof(work_profile.matrix_in_transposed)); + assert_int_equal(mat3SSEinv(work_profile.matrix_out_transposed, + work_profile.matrix_in_transposed), 0); + dt_dev_pixelpipe_t pipe = { .input_profile_info = &profile, + .work_profile_info = &work_profile }; + dt_dev_pixelpipe_iop_t piece = { .data = &d, .colors = 4, .pipe = &pipe }; + const dt_iop_roi_t roi = { .width = 1, .height = 1, .scale = 1.0f }; + const float DT_ALIGNED_ARRAY colored[] = { 0.5f, 0.25f, 0.25f, 1.0f }; + const float DT_ALIGNED_ARRAY gray[] = { 0.4f, 0.4f, 0.4f, 1.0f }; + float DT_ALIGNED_ARRAY out[4]; + const float sat_scales[] = { -0.1f, 1.0f, 1.3f, 1.31f, 2.0f }; + const float expected_green[] = { 0.5f, 0.25f, 0.175f, 0.1725f, 0.0f }; + const float val_scales[] = { -0.1f, 1.0f, 1.2f, 1.21f, 2.0f }; + for(int i = 0; i < 5; i++) + { + table[1] = table[4] = sat_scales[i]; + table[2] = table[5] = 1.0f; + process(&module, &piece, colored, out, &roi, &roi); + assert_float_equal(out[1], expected_green[i], 1e-6f); + table[1] = table[4] = 1.0f; + table[2] = table[5] = val_scales[i]; + process(&module, &piece, gray, out, &roi, &roi); + assert_float_equal(out[0], gray[0], 1e-6f); + } + const float DT_ALIGNED_ARRAY bright[] = { 0.9f, 0.9f, 0.9f, 1.0f }; + process(&module, &piece, bright, out, &roi, &roi); + assert_float_equal(out[0], bright[0], 1e-6f); +} + +static void test_processing(void **state) +{ + float table[] = { 120.0f, 0.5f, 0.5f, 120.0f, 0.5f, 0.5f }; + dt_iop_dng_look_data_t d = { .hsm = table, .hue_div = 1, .sat_div = 2, .val_div = 1 }; + dt_iop_module_t module = { 0 }; + dt_iop_order_iccprofile_info_t profile = { .type = DT_COLORSPACE_FORWARD_MATRIX }; + dt_iop_order_iccprofile_info_t work_profile = { 0 }; + memcpy(work_profile.matrix_in_transposed, prophotorgb_to_xyz_transpose, + sizeof(work_profile.matrix_in_transposed)); + assert_int_equal(mat3SSEinv(work_profile.matrix_out_transposed, + work_profile.matrix_in_transposed), 0); + dt_dev_pixelpipe_t pipe = { .input_profile_info = &profile, + .work_profile_info = &work_profile }; + dt_dev_pixelpipe_iop_t piece = { .data = &d, .colors = 4, .pipe = &pipe }; + const dt_iop_roi_t roi = { .width = 1, .height = 2, .scale = 1.0f }; + const float DT_ALIGNED_ARRAY in[] = { 1.0f, 0.0f, 0.0f, 0.37f, 0.4f, 0.4f, 0.4f, 0.81f }; + float DT_ALIGNED_ARRAY out[8]; + process(&module, &piece, in, out, &roi, &roi); + assert_float_equal(out[0], 0.5f, 1e-6f); + assert_float_equal(out[1], 1.0f, 1e-6f); + assert_float_equal(out[3], in[3], 1e-6f); + assert_float_equal(out[4], 0.4f, 1e-6f); + assert_float_equal(out[7], in[7], 1e-6f); + + profile.type = DT_COLORSPACE_DNG_LOOK; + process(&module, &piece, in, out, &roi, &roi); + assert_float_equal(out[0], 0.5f, 1e-6f); + assert_float_equal(out[1], 1.0f, 1e-6f); + assert_float_equal(out[3], in[3], 1e-6f); + + profile.type = DT_COLORSPACE_EMBEDDED_MATRIX; + process(&module, &piece, in, out, &roi, &roi); + assert_memory_equal(out, in, sizeof(in)); + pipe.input_profile_info = NULL; + process(&module, &piece, in, out, &roi, &roi); + assert_memory_equal(out, in, sizeof(in)); + profile.type = DT_COLORSPACE_FORWARD_MATRIX; + pipe.input_profile_info = &profile; + + d.hsm = NULL; + process(&module, &piece, in, out, &roi, &roi); + assert_memory_equal(out, in, sizeof(in)); +} + +static void _init_work_profile(dt_iop_order_iccprofile_info_t *work_profile, + const cmsCIExyY *whitepoint, + const cmsCIExyYTRIPLE *primaries) +{ + cmsToneCurve *gamma = cmsBuildGamma(NULL, 1.0); + assert_non_null(gamma); + cmsToneCurve *curves[] = { gamma, gamma, gamma }; + cmsHPROFILE profile = cmsCreateRGBProfile(whitepoint, primaries, curves); + cmsFreeToneCurve(gamma); + assert_non_null(profile); + const cmsTagSignature tags[] = { cmsSigRedColorantTag, cmsSigGreenColorantTag, + cmsSigBlueColorantTag }; + for(int c = 0; c < 3; c++) + { + const cmsCIEXYZ *colorant = cmsReadTag(profile, tags[c]); + assert_non_null(colorant); + work_profile->matrix_in_transposed[c][0] = colorant->X; + work_profile->matrix_in_transposed[c][1] = colorant->Y; + work_profile->matrix_in_transposed[c][2] = colorant->Z; + } + assert_int_equal(mat3SSEinv(work_profile->matrix_out_transposed, + work_profile->matrix_in_transposed), 0); + cmsCloseProfile(profile); +} + +static void _apply_direct_look(const dt_iop_dng_look_data_t *d, + const dt_aligned_pixel_t rgb, + dt_aligned_pixel_t out) +{ + dt_aligned_pixel_t hsv; + dt_aligned_pixel_t correction; + dt_RGB_2_HSV(rgb, hsv); + _lookup_hsm(d, hsv, correction); + hsv[0] += correction[0] / 360.0f; + hsv[0] -= floorf(hsv[0]); + hsv[1] = CLIP(hsv[1] * correction[1]); + dt_HSV_2_RGB(hsv, out); + out[3] = rgb[3]; +} + +static void test_work_profile_independence(void **state) +{ + float table[36 * 3 * 2 * 3]; + for(int v = 0; v < 2; v++) + for(int h = 0; h < 36; h++) + for(int s = 0; s < 3; s++) + { + const int index = 3 * ((v * 36 + h) * 3 + s); + table[index] = 2.0f * h + 3.0f * s + 5.0f * v; + table[index + 1] = 0.5f + 0.01f * h + 0.1f * s + 0.05f * v; + table[index + 2] = 2.0f; + } + dt_iop_dng_look_data_t d = { .hsm = table, .hue_div = 36, .sat_div = 3, .val_div = 2 }; + dt_iop_module_t module = { 0 }; + dt_iop_order_iccprofile_info_t input_profile = { .type = DT_COLORSPACE_FORWARD_MATRIX }; + dt_dev_pixelpipe_t pipe = { .input_profile_info = &input_profile }; + dt_dev_pixelpipe_iop_t piece = { .data = &d, .colors = 4, .pipe = &pipe }; + const dt_iop_roi_t roi = { .width = 1, .height = 1, .scale = 1.0f }; + const cmsCIExyY whitepoints[] = { { 0.3127, 0.3290, 1.0 }, { 0.3127, 0.3290, 1.0 }, + { 0.3457, 0.3585, 1.0 } }; + const cmsCIExyYTRIPLE primaries[] = { + { { 0.64, 0.33, 1.0 }, { 0.30, 0.60, 1.0 }, { 0.15, 0.06, 1.0 } }, + { { 0.708, 0.292, 1.0 }, { 0.170, 0.797, 1.0 }, { 0.131, 0.046, 1.0 } }, + { { 0.7347, 0.2653, 1.0 }, { 0.1596, 0.8404, 1.0 }, { 0.0366, 0.0001, 1.0 } }, + }; + const dt_aligned_pixel_t stimuli[] = { + { 0.6f, 0.25f, 0.1f, 0.37f }, + { 0.1f, 0.6f, 0.25f, 0.81f }, + { 0.25f, 0.1f, 0.6f, 0.5f }, + }; + gboolean different_raw_bin = FALSE; + for(int sample = 0; sample < (int)G_N_ELEMENTS(stimuli); sample++) + { + dt_aligned_pixel_t XYZ, look_rgb, expected, reference_hsv; + dt_linearRGB_to_XYZ(stimuli[sample], XYZ); + dt_XYZ_to_prophotorgb(XYZ, look_rgb); + look_rgb[3] = stimuli[sample][3]; + dt_RGB_2_HSV(look_rgb, reference_hsv); + _apply_direct_look(&d, look_rgb, expected); + for(int profile = 0; profile < (int)G_N_ELEMENTS(primaries); profile++) + { + dt_iop_order_iccprofile_info_t work_profile = { 0 }; + _init_work_profile(&work_profile, &whitepoints[profile], &primaries[profile]); + pipe.work_profile_info = &work_profile; + dt_aligned_pixel_t in, out, raw_hsv, corrected_XYZ, corrected_rgb; + dt_apply_transposed_color_matrix(XYZ, work_profile.matrix_out_transposed, in); + in[3] = stimuli[sample][3]; + dt_RGB_2_HSV(in, raw_hsv); + different_raw_bin |= (int)(raw_hsv[0] * d.hue_div) + != (int)(reference_hsv[0] * d.hue_div); + process(&module, &piece, in, out, &roi, &roi); + dt_apply_transposed_color_matrix(out, work_profile.matrix_in_transposed, corrected_XYZ); + dt_XYZ_to_prophotorgb(corrected_XYZ, corrected_rgb); + for_three_channels(c) + assert_float_equal(corrected_rgb[c], expected[c], 2e-6f); + assert_float_equal(out[3], in[3], 0.0f); + } + } + assert_true(different_raw_bin); +} + +static void test_null_and_prophoto_work_profiles(void **state) +{ + float table[] = { 30.0f, 0.6f, 2.0f, 90.0f, 1.4f, 0.5f }; + dt_iop_dng_look_data_t d = { .hsm = table, .hue_div = 1, .sat_div = 2, .val_div = 1 }; + dt_iop_module_t module = { 0 }; + dt_iop_order_iccprofile_info_t input_profile = { .type = DT_COLORSPACE_FORWARD_MATRIX }; + dt_dev_pixelpipe_t pipe = { .input_profile_info = &input_profile }; + dt_dev_pixelpipe_iop_t piece = { .data = &d, .colors = 4, .pipe = &pipe }; + const dt_iop_roi_t roi = { .width = 1, .height = 1, .scale = 1.0f }; + dt_iop_order_iccprofile_info_t work_profile = { 0 }; + memcpy(work_profile.matrix_in_transposed, prophotorgb_to_xyz_transpose, + sizeof(work_profile.matrix_in_transposed)); + assert_int_equal(mat3SSEinv(work_profile.matrix_out_transposed, + work_profile.matrix_in_transposed), 0); + const dt_aligned_pixel_t stimuli[] = { + { 0.6f, 0.25f, 0.1f, 0.37f }, { 0.1f, 0.6f, 0.25f, 0.81f }, + { 0.25f, 0.1f, 0.6f, 0.5f }, { 0.4f, 0.4f, 0.4f, 1.0f }, + { 0.0f, 0.0f, 0.0f, 0.0f }, + }; + for(int sample = 0; sample < (int)G_N_ELEMENTS(stimuli); sample++) + { + dt_aligned_pixel_t expected, out; + pipe.work_profile_info = NULL; + assert_null(dt_ioppr_get_pipe_work_profile_info(&pipe)); + process(&module, &piece, stimuli[sample], out, &roi, &roi); + assert_memory_equal(out, stimuli[sample], sizeof(out)); + _apply_direct_look(&d, stimuli[sample], expected); + pipe.work_profile_info = &work_profile; + process(&module, &piece, stimuli[sample], out, &roi, &roi); + for_each_channel(c) + assert_float_equal(out[c], expected[c], 2e-6f); + assert_float_equal(out[3], expected[3], 0.0f); + } +} + +static void test_commit_and_defaults(void **state) +{ + dt_develop_t dev = { 0 }; + dt_iop_module_t module = { .dev = &dev }; + dt_dev_pixelpipe_t pipe = { 0 }; + dt_dev_pixelpipe_iop_t piece = { .colors = 4 }; + init_pipe(&module, &pipe, &piece); + dt_iop_dng_look_data_t *d = piece.data; + reload_defaults(&module); + assert_false(module.default_enabled); + + float table[] = { 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; + dev.image_storage.profile_hsm_data = table; + dev.image_storage.profile_hsm_hue_div = 1; + dev.image_storage.profile_hsm_sat_div = 2; + dev.image_storage.profile_hsm_val_div = 1; + reload_defaults(&module); + assert_false(module.default_enabled); + commit_params(&module, NULL, &pipe, &piece); + assert_memory_equal(d->hsm, table, sizeof(table)); + assert_false(piece.process_cl_ready); + table[0] = 60.0f; + assert_float_equal(d->hsm[0], 0.0f, 1e-6f); + + dev.image_storage.profile_hsm_hue_div = G_MAXINT; + commit_params(&module, NULL, &pipe, &piece); + assert_null(d->hsm); + dev.image_storage.profile_hsm_hue_div = 1; + table[1] = NAN; + commit_params(&module, NULL, &pipe, &piece); + assert_null(d->hsm); + table[1] = -1.0f; + commit_params(&module, NULL, &pipe, &piece); + assert_null(d->hsm); + + dev.image_storage.profile_hsm_data = NULL; + commit_params(&module, NULL, &pipe, &piece); + cleanup_pipe(&module, &pipe, &piece); +} + +static void *_colorin_get_p(const void *params, const char *name) +{ + assert_string_equal(name, "type"); + return (void *)params; +} + +static void test_automatic_enablement(void **state) +{ + dt_colorspaces_color_profile_type_t type = DT_COLORSPACE_FORWARD_MATRIX; + dt_colorspaces_color_profile_type_t stale_type = DT_COLORSPACE_EMBEDDED_MATRIX; + dt_iop_module_so_t colorin_so = { .op = "colorin" }; + dt_iop_module_t colorin = { .so = &colorin_so, .get_p = _colorin_get_p, + .default_params = &type, .params = &stale_type, + .default_enabled = TRUE }; + dt_develop_t dev = { 0 }; + dev.iop = g_list_append(NULL, &colorin); + dt_iop_dng_look_params_t defaults = { 0 }; + dt_iop_dng_look_params_t history_params = { 0 }; + dt_iop_module_t module = { .op = "dng_look", .dev = &dev, .default_params = &defaults }; + dt_dev_pixelpipe_t pipe = { 0 }; + dt_dev_pixelpipe_iop_t piece = { 0 }; + init_pipe(&module, &pipe, &piece); + + float table[] = { 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; + dev.image_storage.profile_hsm_hue_div = 1; + dev.image_storage.profile_hsm_sat_div = 2; + dev.image_storage.profile_hsm_val_div = 1; + + const dt_colorspaces_color_profile_type_t types[] = + { DT_COLORSPACE_FORWARD_MATRIX, DT_COLORSPACE_DNG_LOOK, + DT_COLORSPACE_EMBEDDED_MATRIX, DT_COLORSPACE_LIN_REC709 }; + for(int matrix = 0; matrix < 2; matrix++) + for(int profile = 0; profile < (int)G_N_ELEMENTS(types); profile++) + for(int data = 0; data < 2; data++) + { + if(matrix) + dev.image_storage.dng_forward_matrix[0] = 1.0f; + else + dt_mark_colormatrix_invalid(&dev.image_storage.dng_forward_matrix[0]); + type = types[profile]; + stale_type = type == DT_COLORSPACE_EMBEDDED_MATRIX + ? DT_COLORSPACE_FORWARD_MATRIX : DT_COLORSPACE_EMBEDDED_MATRIX; + dev.image_storage.profile_hsm_data = data & 1 ? table : NULL; + module.default_enabled = TRUE; + reload_defaults(&module); + commit_params(&module, module.default_params, &pipe, &piece); + assert_int_equal(piece.enabled, + ((matrix && type == DT_COLORSPACE_FORWARD_MATRIX) + || type == DT_COLORSPACE_DNG_LOOK) && data != 0); + } + + type = DT_COLORSPACE_FORWARD_MATRIX; + dt_colorspaces_color_profile_type_t embedded = DT_COLORSPACE_EMBEDDED_MATRIX; + dt_dev_history_item_t hist1 = { .module = &colorin, .params = &embedded, .enabled = TRUE }; + dt_dev_history_item_t hist2 = { .module = &colorin, .params = &type, .enabled = TRUE }; + dev.history = g_list_append(NULL, &hist1); + dev.history = g_list_append(dev.history, &hist2); + dev.history_end = 1; + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + dev.history_end = 2; + commit_params(&module, module.default_params, &pipe, &piece); + assert_true(piece.enabled); + hist2.enabled = FALSE; + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + hist2.enabled = TRUE; + + piece.enabled = FALSE; + commit_params(&module, &history_params, &pipe, &piece); + assert_false(piece.enabled); + piece.enabled = TRUE; + commit_params(&module, &history_params, &pipe, &piece); + assert_true(piece.enabled); + + dev.module_filter_out = g_list_append(NULL, "exposure"); + commit_params(&module, module.default_params, &pipe, &piece); + assert_true(piece.enabled); + dev.module_filter_out = g_list_append(dev.module_filter_out, "dng_look"); + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + g_list_free(dev.module_filter_out); + dev.module_filter_out = g_list_append(NULL, "colorin"); + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + g_list_free(dev.module_filter_out); + dev.module_filter_out = NULL; + + colorin.get_p = NULL; + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + g_list_free(dev.iop); + dev.iop = NULL; + commit_params(&module, module.default_params, &pipe, &piece); + assert_false(piece.enabled); + + assert_true(flags() & IOP_FLAGS_HIDDEN); + cleanup_pipe(&module, &pipe, &piece); + g_list_free(dev.history); +} + +static void test_profile_switch_tracking(void **state) +{ + dt_colorspaces_color_profile_type_t default_type = DT_COLORSPACE_EMBEDDED_MATRIX; + dt_colorspaces_color_profile_type_t type = DT_COLORSPACE_FORWARD_MATRIX; + dt_iop_module_so_t colorin_so = { .op = "colorin" }; + dt_iop_module_t colorin = { .so = &colorin_so, .get_p = _colorin_get_p, + .default_params = &default_type, .default_enabled = TRUE }; + dt_develop_t dev = { 0 }; + dev.iop = g_list_append(NULL, &colorin); + dt_iop_dng_look_params_t defaults = { 0 }; + dt_iop_dng_look_params_t history_params = { 0 }; + dt_iop_module_t module = { .op = "dng_look", .dev = &dev, .default_params = &defaults }; + dt_dev_pixelpipe_t pipe = { 0 }; + dt_dev_pixelpipe_iop_t piece = { 0 }; + init_pipe(&module, &pipe, &piece); + reload_defaults(&module); + + float table[] = { 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; + dev.image_storage.profile_hsm_hue_div = 1; + dev.image_storage.profile_hsm_sat_div = 2; + dev.image_storage.profile_hsm_val_div = 1; + + // the combobox rewrites colorin's top history item in place + dt_dev_history_item_t colorin_hist = { .module = &colorin, .params = &type, .enabled = TRUE }; + dev.history = g_list_append(NULL, &colorin_hist); + dev.history_end = 1; + dt_dev_history_item_t look_hist = { .module = &module, .params = &history_params, .enabled = TRUE }; + const dt_colorspaces_color_profile_type_t switches[] = + { DT_COLORSPACE_FORWARD_MATRIX, DT_COLORSPACE_EMBEDDED_MATRIX, DT_COLORSPACE_DNG_LOOK, + DT_COLORSPACE_STANDARD_MATRIX, DT_COLORSPACE_DNG_LOOK, DT_COLORSPACE_FORWARD_MATRIX, + DT_COLORSPACE_DNG_LOOK, DT_COLORSPACE_EMBEDDED_MATRIX, DT_COLORSPACE_FORWARD_MATRIX }; + + for(int own_history = 0; own_history < 2; own_history++) + { + if(own_history) + { + dev.history = g_list_append(dev.history, &look_hist); + dev.history_end = 2; + } + for(int matrix = 0; matrix < 2; matrix++) + { + if(matrix) + dev.image_storage.dng_forward_matrix[0] = 1.0f; + else + dt_mark_colormatrix_invalid(&dev.image_storage.dng_forward_matrix[0]); + for(int i = 0; i < (int)G_N_ELEMENTS(switches); i++) + { + type = switches[i]; + const gboolean expected = (matrix && type == DT_COLORSPACE_FORWARD_MATRIX) + || type == DT_COLORSPACE_DNG_LOOK; + dt_iop_params_t *params = own_history ? look_hist.params : module.default_params; + + // missing data disables the piece, but a full sync must recover once data is present + dev.image_storage.profile_hsm_data = NULL; + piece.enabled = own_history ? look_hist.enabled : module.default_enabled; + commit_params(&module, params, &pipe, &piece); + assert_false(piece.enabled); + dev.image_storage.profile_hsm_data = table; + for(int previous_enabled = 0; previous_enabled < 2; previous_enabled++) + { + piece.enabled = previous_enabled; + // history replay restores the user's enabled flag before committing the piece + if(own_history) piece.enabled = look_hist.enabled; + commit_params(&module, params, &pipe, &piece); + assert_int_equal(piece.enabled, expected); + commit_params(&module, params, &pipe, &piece); + assert_int_equal(piece.enabled, expected); + } + } + } + } + + // an explicit disable in the look's history wins for both look-capable profiles + dev.image_storage.dng_forward_matrix[0] = 1.0f; + for(int i = 0; i < 2; i++) + { + type = i ? DT_COLORSPACE_DNG_LOOK : DT_COLORSPACE_FORWARD_MATRIX; + for(int enabled = 0; enabled < 2; enabled++) + { + look_hist.enabled = enabled; + piece.enabled = look_hist.enabled; + commit_params(&module, look_hist.params, &pipe, &piece); + assert_int_equal(piece.enabled, enabled); + } + } + + cleanup_pipe(&module, &pipe, &piece); + g_list_free(dev.history); + g_list_free(dev.iop); +} + +static void test_module_order(void **state) +{ + for(int version = DT_IOP_ORDER_LEGACY; version < DT_IOP_ORDER_LAST; version++) + { + GList *list = dt_ioppr_get_iop_order_list_version(version); + gboolean found = FALSE; + for(const GList *l = list; l; l = l->next) + { + const dt_iop_order_entry_t *entry = l->data; + if(!strcmp(entry->operation, "colorin")) + { + assert_non_null(l->next); + entry = l->next->data; + assert_string_equal(entry->operation, "dng_look"); + found = TRUE; + break; + } + } + assert_true(found); + g_list_free_full(list, free); + } +} + +static void test_cache_snapshot(void **state) +{ + float table[] = { 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; + dt_image_t image = { .id = 42, .profile_hsm_data = table, + .profile_hsm_hue_div = 1, .profile_hsm_sat_div = 2, .profile_hsm_val_div = 1 }; + dt_develop_t dev = { 0 }; + dev.image_storage.id = image.id; + dt_iop_dng_look_params_t params = { 0 }; + dt_iop_module_t module = { .dev = &dev }; + dt_dev_pixelpipe_t pipe = { 0 }; + dt_dev_pixelpipe_iop_t piece = { .colors = 4 }; + cached_image = ℑ + cache_locked = FALSE; + init_pipe(&module, &pipe, &piece); + commit_params(&module, ¶ms, &pipe, &piece); + dt_iop_dng_look_data_t *d = piece.data; + assert_false(cache_locked); + assert_memory_equal(d->hsm, table, sizeof(table)); + table[0] = 90.0f; + assert_float_equal(d->hsm[0], 0.0f, 1e-6f); + cleanup_pipe(&module, &pipe, &piece); + cached_image = NULL; +} + +int main(int argc, char *argv[]) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_interpolation_and_hue_wrap), + cmocka_unit_test(test_full_saturation_and_unchanged_value), + cmocka_unit_test(test_processing), + cmocka_unit_test(test_work_profile_independence), + cmocka_unit_test(test_null_and_prophoto_work_profiles), + cmocka_unit_test(test_commit_and_defaults), + cmocka_unit_test(test_automatic_enablement), + cmocka_unit_test(test_profile_switch_tracking), + cmocka_unit_test(test_module_order), + cmocka_unit_test(test_cache_snapshot), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} +// 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