diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 55005013234a..3d3c926cf7b0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -134,6 +134,7 @@ FILE(GLOB SOURCE_FILES "develop/tiling.c" "common/dwt.c" "common/heal.c" + "common/colorchecker.c" "develop/masks/brush.c" "develop/masks/circle.c" "develop/masks/group.c" diff --git a/src/common/colorchecker.c b/src/common/colorchecker.c new file mode 100644 index 000000000000..15b512278bd8 --- /dev/null +++ b/src/common/colorchecker.c @@ -0,0 +1,1405 @@ +/* + This file is part of darktable, + Copyright (C) 2020 darktable developers, + Copyright (C) 2025 Guillaume Stutin. + + 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 . +*/ + +/** + * ANSI CGATS.17 is THE standard text file format for exchanging color measurement data. + * This standard text format (the ASCII version is by far the most common) is the format + * accepted by most color measurement and profiling applications. + * They can be used with lcms2. + * + * IT8 targets contain 288 patches in total. + * At the bottom of the chart, there is a grey scale consisting of 22 patches (labeled GS01 to GS22), + * flanked on each side by a Dmin and a Dmax patch, which are usually + * labeled as Dmin or GS0, and Dmax or GS23. + */ + +#include "colorchecker.h" +#include "common/colorspaces_inline_conversions.h" +#include "darktable.h" +#include "file_location.h" + +#include +#include +#include + +#define ERROR \ + { \ + lineno = __LINE__; \ + goto error; \ + } + +typedef enum parser_state_t { + BLOCK_NONE = 0, + BLOCK_BOXES, + BLOCK_BOX_SHRINK, + BLOCK_REF_ROTATION, + BLOCK_XLIST, + BLOCK_YLIST, + BLOCK_EXPECTED +} parser_state_t; + +typedef struct cht_box_t { + char key_letter; // 'D', 'X', or 'Y' + char *label_x_start; + char *label_x_end; + char *label_y_start; + char *label_y_end; + float width; + float height; + float x_origin; + float y_origin; + float x_increment; + float y_increment; +} cht_box_t; + +typedef struct cht_box_F_t { + float ax; // top left corner + float ay; // top left corner + float bx; // top right corner + float by; // top right corner + float cx; // bottom left corner + float cy; // bottom left corner + float dx; // bottom right corner + float dy; // bottom right corner + float width; // width of the frame + float height; // height of the frame +} cht_box_F_t; + +#define MAX_LINE_LENGTH 512 +#define TWO_SQRT2f 2.8284271247461900976f // sqrt(2) * 2 + +static void _dt_color_checker_patch_copy(dt_color_checker_patch *dest, const dt_color_checker_patch *src) +{ + if(!dest || !src) return; + + dest->name = g_strdup(src->name); + dest->x = src->x; + dest->y = src->y; + dest->Lab[0] = src->Lab[0]; + dest->Lab[1] = src->Lab[1]; + dest->Lab[2] = src->Lab[2]; +} + +void dt_color_checker_copy(dt_color_checker_t *dest, const dt_color_checker_t *src) +{ + if(!dest || !src) return; + + dest->name = g_strdup(src->name); + dest->author = g_strdup(src->author); + dest->date = g_strdup(src->date); + dest->manufacturer = g_strdup(src->manufacturer); + dest->type = src->type; + dest->radius = src->radius; + dest->ratio = src->ratio; + dest->patches = src->patches; + dest->size[0] = src->size[0]; + dest->size[1] = src->size[1]; + dest->middle_grey = src->middle_grey; + dest->white = src->white; + dest->black = src->black; + + if(src->values) + { + dest->values = dt_color_checker_patch_array_init(src->patches); + if(!dest->values) + { + fprintf(stderr, "Error: Memory allocation failed for color checker values.\n"); + return; + } + + for(int i = 0; i < src->patches; i++) + { + _dt_color_checker_patch_copy(&dest->values[i], &src->values[i]); + } + } + else + { + dest->values = NULL; + } +} + +static cht_box_F_t *_dt_cht_extract_F(const char **tokens) +{ + cht_box_F_t *frame_coordinates = (cht_box_F_t *)malloc(sizeof(cht_box_F_t)); + if(!frame_coordinates) return NULL; + + size_t index = 0; + float extracted_coords[8] = { 0.f }; + for(size_t i = 0; tokens[i] != NULL && index < 8; i++) + { + if(g_ascii_isdigit(tokens[i][0])) // note : always a positive number + { + extracted_coords[index] = (float)g_ascii_strtod(tokens[i], NULL); + index++; + } + } + + frame_coordinates->ax = extracted_coords[0]; + frame_coordinates->ay = extracted_coords[1]; + frame_coordinates->bx = extracted_coords[2]; + frame_coordinates->by = extracted_coords[3]; + frame_coordinates->cx = extracted_coords[4]; + frame_coordinates->cy = extracted_coords[5]; + frame_coordinates->dx = extracted_coords[6]; + frame_coordinates->dy = extracted_coords[7]; + frame_coordinates->width = extracted_coords[2] - extracted_coords[0]; + frame_coordinates->height = extracted_coords[5] - extracted_coords[1]; + + return frame_coordinates; +} + +static dt_colorchecker_chart_spec_t *_dt_color_checker_chart_spec_init() +{ + dt_colorchecker_chart_spec_t *result = (dt_colorchecker_chart_spec_t *)malloc(sizeof(dt_colorchecker_chart_spec_t)); + if(!result) return NULL; + + result->patch_width = FLT_MAX; + result->patch_height = FLT_MAX; + result->patches = NULL; + result->address = NULL; + result->guide_size[0] = 0.f; + result->guide_size[1] = 0.f; + + return result; +} + +static void _dt_color_checker_chart_spec_cleanup(dt_colorchecker_chart_spec_t *chart_spec) +{ + if(!chart_spec) return; + if(chart_spec->address == &chart_spec) return; // do not free the static chart_spec + + // Free the patches gslist + if(chart_spec->patches) + g_slist_free_full(chart_spec->patches, dt_color_checker_patch_cleanup_list); + + free(chart_spec); + chart_spec = NULL; +} + +static dt_color_checker_patch *_color_checker_patch_init() +{ + dt_color_checker_patch *patch = (dt_color_checker_patch *)malloc(sizeof(dt_color_checker_patch)); + if(!patch) return NULL; + + patch->name = NULL; + patch->Lab[0] = 0.f; + patch->Lab[1] = 0.f; + patch->Lab[2] = 0.f; + patch->x = -1.f; + patch->y = -1.f; + + return patch; +} + +static void _dt_cht_box_cleanup(void *data) +{ + cht_box_t *box = (cht_box_t *)data; + if(!box) return; + + free(box->label_x_start); + free(box->label_x_end); + free(box->label_y_start); + free(box->label_y_end); + free(box); +} + +static cht_box_t *_dt_cht_box_extract(const char **tokens) +{ + cht_box_t *box = (cht_box_t *)calloc(1, sizeof(cht_box_t)); + if(!box) return NULL; + + size_t index = 0; + for(size_t i = 0; tokens[i] != NULL && index < 11; i++) + { + if(tokens[i][0] != '\0') + { + float value = 0; + const char *string = tokens[i]; + + if(g_ascii_isdigit(tokens[i][0]) || tokens[i][0] == '-') + value = (float)g_ascii_strtod(tokens[i], NULL); + + switch(index) + { + case 0: box->key_letter = tokens[i][0]; index++; break; // 'D', 'X', or 'Y' + case 1: box->label_x_start = g_strdup(string); index++; break; + case 2: box->label_x_end = g_strdup(string); index++; break; + case 3: box->label_y_start = g_strdup(string); index++; break; + case 4: box->label_y_end = g_strdup(string); index++; break; + case 5: box->width = value; index++; break; + case 6: box->height = value; index++; break; + case 7: box->x_origin = value; index++; break; + case 8: box->y_origin = value; index++; break; + case 9: box->x_increment = value; index++; break; + case 10: box->y_increment = value; index++; break; + } + } + } + + return box; +} + +/** + * @brief Increments a string alphanumerically. + * + * @param in The input string to increment. + * @return char* A new string with the last character incremented. The caller is responsible for freeing the returned string. + */ +static char *_increment_string(gchar *in) +{ + if (!in || *in == '\0') return NULL; + + gchar *result = g_strdup(in); + size_t len = safe_strlen(result); + + if(len == 0) return NULL; + + for(int i = (int)len - 1; i >= 0; i--) + { + // for numbers + if (g_ascii_isdigit(result[i])) + { + if (result[i] == '9') + { + result[i] = '0'; + continue; + } + result[i]++; + break; + } + // for letters + else if (g_ascii_isalpha(result[i])) + { + if (result[i] == 'z' || result[i] == 'Z') + { + result[i] = (result[i] == 'z') ? 'a' : 'A'; + continue; + } + result[i]++; + break; + } + // there should not be other cases + else + { + break; + } + } + + return result; +} + +/** + * @brief Removes leading zeros from a string. + * + * @param in The input string. + * @return char* A new string with leading zeros removed. The caller is responsible for freeing the returned string. + */ +static inline const char *_remove_leading_zeros(const char *in) +{ + if(!in || *in == '\0') return ""; + const char *start = in; + while(*start == '0') start++; + + return start; +} + +/** + * @brief Generates a list of patches from the provided cht_patch structure. + * Patche's positions are calculated by iterating over the labels alphanumerically. + * + * @param cht_patch The structure containing the patch information. + * @param chart The structure to populate with patches. + * @param F_box The cht_box_F_t structure containing the frame values. + * @return gboolean Returns TRUE if the operation was successful, FALSE otherwise. + */ +static gboolean _dt_cht_generate_patch_list(const cht_box_t *cht_patch, dt_colorchecker_chart_spec_t *chart, const cht_box_F_t *F_box) +{ + gboolean result = FALSE; + int lineno = 0; + + gchar *current_frst = NULL; + gchar *current_scnd = NULL; + gchar *last_label = NULL; + + // Input validation + if(!cht_patch) + { + fprintf(stderr, "Invalid cht_patch"); + ERROR; + } + + if(!chart) + { + fprintf(stderr, "Invalid chart"); + ERROR; + } + + // The key letter determines the axes to begin to iterate + gboolean swap_axes = (cht_patch->key_letter == 'Y') ? TRUE : FALSE; + + // Unpack strings from cht_patch + const char *start_colum = swap_axes ? cht_patch->label_y_start : cht_patch->label_x_start; + const char *end_colum = swap_axes ? cht_patch->label_y_end : cht_patch->label_x_end; + + const char *start_row = swap_axes ? cht_patch->label_x_start : cht_patch->label_y_start; + const char *end_row = swap_axes ? cht_patch->label_x_end : cht_patch->label_y_end; + + // start shouldn't be greater than end + if(g_strcmp0(start_colum, end_colum) > 0 || g_strcmp0(start_row, end_row) > 0) + ERROR + + // we want the center of the patch. + const float patch_w = cht_patch->width / 2; + const float patch_h = cht_patch->height / 2; + + // Prepare the initial x and y coordinates + float origin_x = cht_patch->x_origin - (chart->guide_size[0] / 2) + patch_w - F_box->ax; + float origin_y = cht_patch->y_origin - (chart->guide_size[1] / 2) + patch_h - F_box->ay; + + // build last label, for comparison + const char *last_label_colum = (end_colum[0] != '_') ? _remove_leading_zeros(end_colum) : NULL; + const char *last_label_row = (end_row[0] != '_') ? _remove_leading_zeros(end_row) : NULL; + last_label = g_strconcat(last_label_colum ? last_label_colum : "", last_label_row ? last_label_row : "", NULL); + + // Copy string for manipulation + current_frst = g_strdup(start_colum); + const char *end_frst = swap_axes ? cht_patch->label_y_end : cht_patch->label_x_end; + const char *end_scnd = swap_axes ? cht_patch->label_x_end : cht_patch->label_y_end; + if(!current_frst) ERROR + + for(int index_frst = 0; g_strcmp0(current_frst, end_frst) <= 0; index_frst++) + { + current_scnd = g_strdup(start_row); + if(!current_scnd) ERROR + + for(int index_scnd = 0; g_strcmp0(current_scnd, end_scnd) <= 0; index_scnd++) + { + // Create the label + const char *label_frst = current_frst[0] != '_' ? _remove_leading_zeros(current_frst) : NULL; + const char *label_scnd = current_scnd[0] != '_' ? _remove_leading_zeros(current_scnd) : NULL; + + const gchar *label = g_strconcat(label_frst ? label_frst : "", label_scnd ? label_scnd : "", NULL); + if(!label) ERROR + + // Create the patch + dt_color_checker_patch *patch = _color_checker_patch_init(); + if(!patch) ERROR + + // Set the patch properties + patch->name = g_strdup(label); + if(!patch->name) ERROR + + int index_y = swap_axes ? index_frst : index_scnd; + int index_x = swap_axes ? index_scnd : index_frst; + + float temp_x = origin_x + (cht_patch->x_increment * index_x); + temp_x /= F_box->width - chart->guide_size[0]; // normalize to the frame width + + float temp_y = origin_y + (cht_patch->y_increment * index_y); + temp_y /= F_box->height - chart->guide_size[1]; // normalize to the frame height + + patch->x = temp_x; + patch->y = temp_y; + + // Add to the list + chart->patches = g_slist_append(chart->patches, patch); + + if(!g_strcmp0(label, last_label)) goto out; + if(!g_strcmp0(current_scnd, "_")) break; + + // increment x in a new string and pass the ownership to current_scnd + gchar *temp = _increment_string(current_scnd); + g_free(current_scnd); + current_scnd = temp; + + chart->colums = MAX(chart->colums, index_scnd + 1); + } + + // increment y in a new string and pass the ownership to current_frst + gchar *temp = _increment_string(current_frst); + g_free(current_frst); + current_frst = temp; + + chart->rows = MAX(chart->rows, index_frst + 1); + } + +out: + result = TRUE; + goto end; + +error: + fprintf(stderr, "error parsing CHT file, in %s %s:%d\n", __FUNCTION__, __FILE__, lineno); + +end: + g_free(last_label); + g_free(current_scnd); + g_free(current_frst); + return result; +} + +static GList *_parse_cht(const char *filename) +{ + GList *result = NULL; + + int lineno = 0; + GIOChannel *fp = g_io_channel_new_file(filename, "r", NULL); + if(!fp) + { + fprintf(stderr, "Error opening '%s'\n", filename); + return NULL; + } + + // parser control + GString *line = g_string_new(NULL); + parser_state_t last_block = BLOCK_NONE; + int skip_block = 0; + + // main loop over the input file + while(g_io_channel_read_line_string(fp, line, NULL, NULL) == G_IO_STATUS_NORMAL) + { + if(line->len == 0) + { + skip_block = 0; + continue; + } + if(skip_block) continue; + + // we should be at the start of a block now + const char *c = line->str; + while(*c == ' ') c++; // skip leading spaces + gchar **line_tokens = g_strsplit(c, " ", 0); + + if(!g_strcmp0(line_tokens[0], "BOXES") && last_block < BLOCK_BOXES) + { + last_block = BLOCK_BOXES; + + // let's have another loop reading from the file. + while(g_io_channel_read_line_string(fp, line, NULL, NULL) == G_IO_STATUS_NORMAL) + { + if(line->len == 0) break; + + c = line->str; + while(*c == ' ') c++; // skip leading spaces + + gchar **box_tokens = g_strsplit(c, " ", 0); // g_strfreev me with the GList. + if(!g_strcmp0(box_tokens[0], "F") || !g_strcmp0(box_tokens[0], "D") || !g_strcmp0(box_tokens[0], "X") || !g_strcmp0(box_tokens[0], "Y")) + result = g_list_append(result, box_tokens); + + } + } + if(!g_strcmp0(line_tokens[0], "BOX_SHRINK") && last_block < BLOCK_BOX_SHRINK) + { + skip_block = 1; + break; // we don't care about blocks comming after, just skip them. + } + + g_strfreev(line_tokens); + } + + if(last_block == BLOCK_NONE) + ERROR + + goto end; + +error: + fprintf(stderr, "error parsing CHT file, in %s %s:%d\n", __FUNCTION__, __FILE__, lineno); + +end: + if(line) g_string_free(line, TRUE); + if(fp) g_io_channel_unref(fp); + return result; +} + +// according to cht_format.html from argyll: +// "The keywords and associated data must be used in the following order: BOXES, BOX_SHRINK, REF_ROTATION, +// XLIST, YLIST and EXPECTED." +static gboolean _dispatch_cht_data(GList **boxes, dt_colorchecker_chart_spec_t *chart_spec) +{ + gboolean result = FALSE; + int lineno = 0; + + // data gathered from the CHT file + cht_box_F_t *F_box = NULL; + GList *boxes_list = NULL; + + float chart_radius = -1.f; + + for(GList *lines = *boxes; lines; lines = g_list_next(lines)) + { + const char **tokens = (const char **)lines->data; + if(!tokens) ERROR + + const char letter = tokens[0][0]; + if(letter == 'F') + { + F_box = _dt_cht_extract_F(tokens); + } + + else if(letter == 'D' || letter == 'X' || letter == 'Y') + { + cht_box_t *box = _dt_cht_box_extract(tokens); + if(!box) ERROR + + boxes_list = g_list_append(boxes_list, box); + } + } + + if(!F_box) ERROR + + // Fill the colorchecker spec structure + chart_spec->ratio = F_box->height / F_box->width; + chart_radius = hypotf(F_box->height, F_box->width); + + for(GList *iter = boxes_list; iter; iter = g_list_next(iter)) + { + cht_box_t *box = (cht_box_t *)iter->data; + if(!box) ERROR + + if(box->key_letter == 'D') + { + // Save the guide corner sizes when they are specified, to changes the patches area size in consequence. + if(!g_strcmp0(box->label_x_start,"MARK")) chart_spec->guide_size[0] = box->width - box->x_origin; + if(!g_strcmp0(box->label_x_start,"MARK")) chart_spec->guide_size[1] = box->height - box->y_origin; + } + + else if(box->key_letter == 'X' || box->key_letter == 'Y') + { + chart_spec->patch_width = MIN(chart_spec->patch_width, box->width); + chart_spec->patch_height = MIN(chart_spec->patch_height, box->height); + + if(!_dt_cht_generate_patch_list(box, chart_spec, F_box)) + { + free(box->label_x_start); + free(box->label_x_end); + free(box->label_y_start); + free(box->label_y_end); + free(box); + ERROR + } + } + } + + chart_spec->num_patches = g_slist_length(chart_spec->patches); + chart_spec->size[0] = (size_t)chart_spec->colums; + chart_spec->size[1] = (size_t)chart_spec->rows; + const float patch_radius = hypotf(chart_spec->patch_width, chart_spec->patch_height) / TWO_SQRT2f; + chart_spec->radius = patch_radius / chart_radius; + + result = TRUE; + goto end; + +error: + fprintf(stderr, "Error dispatching CHT file, in %s %s:%d\n", __FUNCTION__, __FILE__, lineno); + +end: + if(F_box) free(F_box); + if(boxes_list) g_list_free_full(boxes_list, _dt_cht_box_cleanup); + + return result; +} + +static gboolean _dt_colorchecker_open_cht(const char *filename, dt_colorchecker_chart_spec_t *chart_spec) +{ + GList *boxes = _parse_cht(filename); + if(!boxes) + { + fprintf(stderr, "Error parsing CHT file '%s'\n", filename); + return FALSE; + } + + if(!_dispatch_cht_data(&boxes, chart_spec)) + { + fprintf(stderr, "Error dispatching CHT data from '%s'\n", filename); + g_list_free_full(boxes, (GDestroyNotify)g_strfreev); + return FALSE; + } + + chart_spec->type = g_path_get_basename(filename); + + g_list_free_full(boxes, (GDestroyNotify)g_strfreev); + + return TRUE; +} + +static inline dt_colorchecker_material_types _dt_colorchecker_IT8_get_material_type(const cmsHANDLE *hIT8) +{ + if(!hIT8) + { + fprintf(stderr, "Error: Invalid IT8 handle provided.\n"); + return COLOR_CHECKER_MATERIAL_UNKNOWN; + } + + const char *CGATS_type = cmsIT8GetSheetType(*hIT8); + if(!CGATS_type) return COLOR_CHECKER_MATERIAL_UNKNOWN; + + if(g_strcmp0(CGATS_type, CGATS_types[CGATS_TYPE_IT8_7_1]) == 0) + return COLOR_CHECKER_MATERIAL_TRANSPARENT; + + else if(g_strcmp0(CGATS_type, CGATS_types[CGATS_TYPE_IT8_7_2]) == 0) + return COLOR_CHECKER_MATERIAL_OPAQUE; + + // if something went wrong + return COLOR_CHECKER_MATERIAL_UNKNOWN; +} + + +/** + * @brief Gets the string representation of the material type ("Transparent" or "Opaque") to be used in label name. + * The caller is responsible for freeing the returned string. + * + * @param material (dt_colorchecker_material_types) the material type of the color checker + * @return gchar* The string representation of the material type, or NULL if unknown. + */ +static inline const char *_dt_colorchecker_get_material_string(const dt_colorchecker_material_types material) +{ + if (material >= COLOR_CHECKER_MATERIAL_TRANSPARENT && material < COLOR_CHECKER_MATERIAL_UNKNOWN) + return colorchecker_material_types[material]; + + // else + return NULL; +} + +static inline dt_colorchecker_CGATS_types _dt_CGATS_get_type_value(const char *type) +{ + dt_colorchecker_CGATS_types t = CGATS_TYPE_IT8_7_1; + + // Ensure t doesn't overflows CGATS_types + while(type && t < CGATS_TYPE_UNKOWN) + { + if(!g_strcmp0(type, CGATS_types[t])) break; + t++; + } + return t; +} + +static dt_colorchecker_chart_spec_t *_dt_colorchecker_get_standard_spec(const char *type) +{ + dt_colorchecker_chart_spec_t *result = NULL; + if(type) + { + const dt_colorchecker_CGATS_types t = _dt_CGATS_get_type_value(type); + + switch(t) + { + case CGATS_TYPE_IT8_7_1: + case CGATS_TYPE_IT8_7_2: + result = &IT8_7; break; + case CGATS_TYPE_UNKOWN: + fprintf(stderr, "Unknown CGATS type: %s\n", type); + result = &IT8_7; break; + } + } + + return result; +} + +/** + * @brief Test if the file is a CGATS.17 file + * and if it contains one table of patch only. + * + * @param data pointer to the cmsHANDLE + * @return gboolean TRUE if the file is valid, FALSE otherwise. + */ +static gboolean _dt_CGATS_is_supported(const cmsHANDLE *hIT8) +{ + gboolean valid = TRUE; + + if(!hIT8) + { + fprintf(stderr, "Error loading IT8 file.\n"); + valid = FALSE; + goto end; + } + else + { + const char *CGATS_type = cmsIT8GetProperty(*hIT8, "CGATS"); + // Check if the data type can be found in our supported list of CGATS types + if(_dt_CGATS_get_type_value(CGATS_type) == CGATS_TYPE_UNKOWN) + { + fprintf(stderr, "Warning: type '%s' is not supported by Ansel.\n", CGATS_type); + valid = FALSE; + goto end; + } + + uint32_t table_count = cmsIT8TableCount(*hIT8); + if(table_count != 1) + { + fprintf(stderr, "Warning: the CGATS file contains %u tables but we only support files" + "with one table at the moment.\n", table_count); + valid = FALSE; + goto end; + } + } + +end: + return valid; +} + +static inline const char *_dt_CGATS_get_author(const cmsHANDLE *hIT8) +{ + if(!hIT8) + { + fprintf(stderr, "Error: Invalid IT8 handle provided.\n"); + return "Unknown Author"; + } + const char *author = cmsIT8GetProperty(*hIT8, "ORIGINATOR"); + + return author ? author : "Unknown Author"; +} + +/** + * @brief Get the production date of the CGATS file. + * + * @param hIT8 the CGATS file handle + * @return const char* a pointer to the date from the CGATS file + */ +static inline const char *_dt_CGATS_get_date(const cmsHANDLE *hIT8) +{ + if(!hIT8) + { + fprintf(stderr, "Error: Invalid IT8 handle provided.\n"); + return "Unknown Date"; + } + + // in CGATS.17, the date in PROD_DATE is stored in the format YYYY:MM + const char *date = cmsIT8GetProperty(*hIT8, "PROD_DATE"); + + return date ? date : "Unknown Date"; +} + +/** + * @brief Modify the date format from "YYYY:MM" given by a CGATS.17 file to the label form "Mon YYYY". + * The caller is responsible for freeing the returned string. + * + * @param date the date in the format "YYYY:MM" + * @return gchar* String with the date in the format "Mon YYYY", or the original date otherwise. + */ +static inline gchar *_dt_CGATS_get_format_date(const cmsHANDLE *hIT8) +{ + if(!hIT8) + { + fprintf(stderr, "Error: Invalid IT8 handle provided.\n"); + return g_strdup("Unknown Date"); + } + + const char *date = _dt_CGATS_get_date(hIT8); + + gchar *result = NULL; + + gchar **parts = g_strsplit_set(date, ":", 0); + if(parts && parts[0] && parts[1]) + { + const char *month[12] + = { "Jan ", "Feb ", "Mar ", "Apr ", "May ", "Jun ", "Jul ", "Aug ", "Sep ", "Oct ", "Nov ", "Dec " }; + int month_num = atoi(parts[1]); + if(month_num >= 1 && month_num <= 12) + parts[1] = g_strdup(month[month_num - 1]); + + // write a new date in the format "Month(short) YYYY" + gchar *date_fmt = g_strdup_printf("%s%s", parts[1], parts[0]); + result = g_strdup(date_fmt); + g_free(date_fmt); + } + else + result = g_strdup(date); + + g_strfreev(parts); + + return result; +} + +static inline const char *_dt_CGATS_get_manufacturer(const cmsHANDLE *hIT8) +{ + if(!hIT8) + { + fprintf(stderr, "Error: Invalid IT8 handle provided.\n"); + return "Unknown Manufacturer"; + } + const char *manufacturer = cmsIT8GetProperty(*hIT8, "MANUFACTURER"); + return manufacturer ? manufacturer : "Unknown Manufacturer"; +} + +/** + * @brief Get the name of a built-in color checker. + * + * @param target_type The type of colorchecker + * @return char* The name of the colorchecker + */ +static inline const char *_dt_get_builtin_colorchecker_name(const dt_color_checker_targets target_type) +{ + dt_color_checker_t *color_checker = dt_get_color_checker(target_type, NULL, NULL); + if(!color_checker) + { + fprintf(stderr, "Error: Unable to get the color checker %d.\n", target_type); + return NULL; + } + const char *name = g_strdup(color_checker->name); + + dt_color_checker_cleanup(color_checker); + return name; +} + +/** + * @brief build a name for the colorchecker. + * The returned string must be freed by the caller. + * + * @param label the struct containing useful data to build a label + * @return gchar* String with the name of the colorchecker + */ +static inline gchar *_dt_colorchecker_label_build_name(const dt_colorchecker_CGATS_label_name_t label) +{ + gchar *name = NULL; + + // Build the name with the format: type (material) - date + gchar *tmp_originator = NULL; + gchar *tmp_date = NULL; + gchar *tmp_material = NULL; + // author if any + if(label.originator && g_strcmp0(label.originator, "")) tmp_originator = g_strdup_printf(" - %s", label.originator); + // date if any + if(label.date && g_strcmp0(label.date, "")) tmp_date = g_strdup_printf(" %s", label.date); + // material if any + if(label.material && g_strcmp0(label.material, "")) tmp_material = g_strdup_printf(" (%s)", label.material); + + // Compose: filename + name = g_strdup_printf("%s%s%s%s", label.type, tmp_material, tmp_date, tmp_originator); + + if(tmp_originator) g_free(tmp_originator); + if(tmp_date) g_free(tmp_date); + + return name; +} + +/** + * @brief Get the name of the colorchecker from the CGATS file. + * The resulting string must be freed by the caller. + * + * @param hIT8 the CGATS file handle + * @param filename the CGATS file name, used if the CGATS file does not contain a name. + * @return char* String with the name of the colorchecker + */ +static inline char *_dt_CGATS_get_name(const cmsHANDLE *hIT8, const char *filename) +{ + gchar *result = NULL; + + if(!hIT8) + { + fprintf(stderr, "dt_CGATS_get_name: Error: Invalid CGATS handle provided.\n"); + return g_strdup("Unnamed CGATS"); + } + + gchar *basename = NULL; + if(filename && g_strcmp0(filename, "")) + { + basename = g_path_get_basename(filename); + char *dot = g_strrstr(basename, "."); + if(dot) + { + // remove the file extension + *dot = '\0'; + } + } + + // Get other useful information from the CGATS file + const char *CGATS_type = cmsIT8GetSheetType(*hIT8); + const dt_colorchecker_chart_spec_t *chart_spec = _dt_colorchecker_get_standard_spec(CGATS_type); + + const char *originator = _dt_CGATS_get_author(hIT8); + const dt_colorchecker_material_types material = _dt_colorchecker_IT8_get_material_type(hIT8); + gchar *material_str = g_strdup(_dt_colorchecker_get_material_string(material)); + gchar *date = _dt_CGATS_get_format_date(hIT8); + + dt_colorchecker_CGATS_label_name_t label = { .type = chart_spec->type, + .originator = originator, + .date = date, + .material = material_str }; //can be NULL + + gchar *name = _dt_colorchecker_label_build_name(label); + + // clean up + g_free(date); + if(material_str) g_free(material_str); + + if(name) + result = name; + else + result = g_strdup(basename && g_strcmp0(basename, "") ? basename : "Unnamed CGATS"); + + if(basename) g_free(basename); + + return result; +} + +static float dE_1976(const float a, const float b, const float c) +{ + return sqrtf(sqf(a) + sqf(b) + sqf(c)); +} + +static inline void _dt_CGATS_find_whitest_blackest_greyest(const dt_color_checker_patch *const values, size_t *bwg, const size_t patch) +{ + for(int i = 0; i < 3; i++) + { + float target = 50.f * i; + float delta_current = dE_1976(values[bwg[i]].Lab[0] - target, values[bwg[i]].Lab[1], values[bwg[i]].Lab[2]); + float delta_patch = dE_1976(values[patch].Lab[0] - target, values[patch].Lab[1], values[patch].Lab[2]); + if(delta_patch < delta_current) + bwg[i] = patch; + } +} + +/** + * @brief fills the patch values from the CGATS file, converts to Lab if needed. + * The number of patches to be filled is given by the CGATS file. + * + * @param hIT8 the CGATS file handle + * @param bwg the array of indices for the black, white, and grey patches + * @param chart_spec the color checker chart specification, used to get the number of patches and patch size + * @param num_patches the number of patches to fill, should be the minimum between the CGATS file and the chart specification + * @return dt_color_checker_patch* a pointer to the array of patches filled with values, or NULL on error. + */ +static dt_color_checker_patch *_dt_colorchecker_CGATS_fill_patch_values(const cmsHANDLE hIT8, size_t *bwg, const dt_colorchecker_chart_spec_t *chart_spec, const size_t num_patches) +{ + int column_SAMPLE_ID = -1; + int column_X = -1; + int column_Y = -1; + int column_Z = -1; + int column_L = -1; + int column_a = -1; + int column_b = -1; + char **sample_names = NULL; + int n_columns = cmsIT8EnumDataFormat(hIT8, &sample_names); + + // Limit the number of patches to the minimum between the CGATS file and the chart specification to avoid overflow. + dt_color_checker_patch *values = dt_color_checker_patch_array_init(num_patches); + if(!values) + { + fprintf(stderr, "Error: Memory allocation failed for values array.\n"); + goto error; + } + + gboolean use_XYZ = FALSE; + if(n_columns == -1) + { + fprintf(stderr, "Error with the CGATS file, can't get column types\n"); + } + + for(int i = 0; i < n_columns; i++) + { + if(!g_strcmp0(sample_names[i], "SAMPLE_ID") || !g_strcmp0(sample_names[i], "SAMPLE_LOC")) + column_SAMPLE_ID = i; + else if(!g_strcmp0(sample_names[i], "XYZ_X")) + column_X = i; + else if(!g_strcmp0(sample_names[i], "XYZ_Y")) + column_Y = i; + else if(!g_strcmp0(sample_names[i], "XYZ_Z")) + column_Z = i; + else if(!g_strcmp0(sample_names[i], "LAB_L")) + column_L = i; + else if(!g_strcmp0(sample_names[i], "LAB_A")) + column_a = i; + else if(!g_strcmp0(sample_names[i], "LAB_B")) + column_b = i; + } + + if(column_SAMPLE_ID == -1) + { + fprintf(stderr, "Error: can't find the SAMPLE_ID column in the CGATS file.\n"); + goto error; + } + + int columns[3] = { -1, -1, -1 }; + if(column_L != -1 && column_a != -1 && column_b != -1) + { + columns[0] = cmsIT8FindDataFormat(hIT8, "LAB_L"); + columns[1] = cmsIT8FindDataFormat(hIT8, "LAB_A"); + columns[2] = cmsIT8FindDataFormat(hIT8, "LAB_B"); + } + // In case no Lab column is found, we assume the IT8 file has XYZ data + else if(column_X != -1 && column_Y != -1 && column_Z != -1) + { + use_XYZ = TRUE; + columns[0] = cmsIT8FindDataFormat(hIT8, "XYZ_X"); + columns[1] = cmsIT8FindDataFormat(hIT8, "XYZ_Y"); + columns[2] = cmsIT8FindDataFormat(hIT8, "XYZ_Z"); + } + else + { + fprintf(stderr, "Error: can't find XYZ or Lab columns in the CGATS file\n"); + goto error; + } + + // Chart dimensions + const int cols = chart_spec->colums; + const int rows = chart_spec->rows; + // Patch size in ratio of the chart size + const float patch_size_x = chart_spec->patch_width; + const float patch_size_y = chart_spec->patch_height; + + // Offset ratio of the center of the patch from the border of the chart + const float patch_offset_x = chart_spec->patch_offset_x; + const float patch_offset_y = chart_spec->patch_offset_y; + + for(size_t patch_iter = 0; patch_iter < num_patches; patch_iter++) + { + // set name + values[patch_iter].name = g_strdup(cmsIT8GetDataRowCol(hIT8, patch_iter, 0)); + if(values[patch_iter].name == NULL) + { + fprintf(stderr, "Error : can't find sample '%lu' in CGATS file\n", patch_iter); + goto error; + } + + // set patch position + if(!chart_spec->address) + { + // The position of the patch is given by the chart specification + const dt_color_checker_patch *p = (dt_color_checker_patch*)g_slist_nth_data(chart_spec->patches, (guint)patch_iter); + if(!p) + { + fprintf(stderr, "Error: patch %lu not found in chart specification.\n", patch_iter); + goto error; + } + _dt_color_checker_patch_copy(&values[patch_iter], p); + } + else + { + // Calculate the patch's position in the chart from buildin data + // IT8 grey scale patches + if(!g_strcmp0(chart_spec->type, "IT8") && patch_iter + 1 > cols * rows) + { + int grey_patches_iter = ((int)patch_iter + 1) - cols * rows; + // calculate the grey patch's horizontal and vertical position in the chart + values[patch_iter].x = ((float)grey_patches_iter - 0.75f) * patch_size_x; + values[patch_iter].y = 14.5f * patch_size_y; + } + else + { + // find the patch's horizontal position in the chart + values[patch_iter].x = (float)(patch_iter % cols) * patch_size_x; + values[patch_iter].x += patch_offset_x; + + // find the patch's vertical position in the chart + values[patch_iter].y = (float)((int)(patch_iter / cols)); // the result must be an integer + values[patch_iter].y *= patch_size_y; + values[patch_iter].y += patch_offset_y; + } + } + + // Copy color values + const double patchdbl[3] = { cmsIT8GetDataRowColDbl(hIT8, (int)patch_iter, columns[0]), + cmsIT8GetDataRowColDbl(hIT8, (int)patch_iter, columns[1]), + cmsIT8GetDataRowColDbl(hIT8, (int)patch_iter, columns[2]) }; + + // Convert to Lab when it's in XYZ + if(use_XYZ) + { + const dt_aligned_pixel_t patch_color = { (float)patchdbl[0] * 0.01, (float)patchdbl[1] *0.01, (float)patchdbl[2] * 0.01, 0.0f }; + dt_XYZ_to_Lab(patch_color, values[patch_iter].Lab); + } + else + { + values[patch_iter].Lab[0] = (float)patchdbl[0]; + values[patch_iter].Lab[1] = (float)patchdbl[1]; + values[patch_iter].Lab[2] = (float)patchdbl[2]; + } + + _dt_CGATS_find_whitest_blackest_greyest(values, bwg, patch_iter); + } + + goto end; + +error: + if(values) + { + for(size_t i = 0; i < num_patches; i++) + { + dt_color_checker_patch_cleanup(&values[i]); + } + } + values = NULL; + +end: + return values; +} + +dt_color_checker_t *dt_colorchecker_user_ref_create(const char *filename, const char *cht_filename) +{ + dt_colorchecker_chart_spec_t *chart_spec = NULL; + gboolean cht_builtin = FALSE; + dt_color_checker_t *checker = NULL; + + int lineno = 0; + + if(!g_file_test(filename, G_FILE_TEST_IS_REGULAR)) + { + fprintf(stderr, "Error: the file '%s' does not exist or is not a regular file.\n", filename); + return NULL; + } + + cmsHANDLE hIT8 = cmsIT8LoadFromFile(NULL, filename); + + if(!_dt_CGATS_is_supported(&hIT8)) + { + fprintf(stderr, "Ansel cannot load the CGATS file '%s'\n", filename); + ERROR + } + + const char *type = cmsIT8GetSheetType(hIT8); + + chart_spec = _dt_color_checker_chart_spec_init(); + if(!chart_spec) + { + fprintf(stderr, "Error: cannot allocate memory for the chart spec.\n"); + ERROR + } + // load the cht file if any + if(cht_filename && g_file_test(cht_filename, G_FILE_TEST_IS_REGULAR)) + { + if(_dt_colorchecker_open_cht(cht_filename, chart_spec)) + cht_builtin = FALSE; + else + { + fprintf(stderr, "Error: cannot open the cht file '%s'.\n", cht_filename); + ERROR + } + } + // if no cht file is provided, use the builtin spec. + else + { + chart_spec = _dt_colorchecker_get_standard_spec(type); + if(chart_spec) + cht_builtin = TRUE; + else + { + fprintf(stderr, "Error: cannot find a chart spec for the CGATS type '%s'.\n", type); + ERROR + } + } + + // Check if the CGATS file contains the expected number of patches + const int num_patches_it8 = (const int)cmsIT8GetPropertyDbl(hIT8, "NUMBER_OF_SETS"); + + if(num_patches_it8 != chart_spec->num_patches) + { + fprintf(stderr, "Warning: the number of patches in the CGATS file (%i) does not match the expected number (%i) in the cht file.\n", + num_patches_it8, chart_spec->num_patches); + } + + // Limit the number of patches to the minimum between the CGATS file and the chart specification to avoid overflow. + const size_t num_patches = MIN(num_patches_it8, chart_spec->num_patches); + fprintf(stderr, "\tOnly %zu patches will be added to the chart\n", num_patches); + + checker = dt_colorchecker_init(); + if(!checker) + { + fprintf(stderr, "Error: cannot allocate memory for the color checker.\n"); + ERROR + } + + checker->name = _dt_CGATS_get_name(&hIT8, filename); + checker->author = g_strdup(_dt_CGATS_get_author(&hIT8)); + checker->date = g_strdup(_dt_CGATS_get_date(&hIT8)); + checker->manufacturer = g_strdup(_dt_CGATS_get_manufacturer(&hIT8)); + checker->type = COLOR_CHECKER_USER_REF; + checker->radius = chart_spec->radius; + checker->ratio = chart_spec->ratio; + checker->patches = num_patches; + checker->size[0] = chart_spec->size[0]; + checker->size[1] = chart_spec->size[1]; + checker->middle_grey = chart_spec->middle_grey; + checker->white = chart_spec->white; + checker->black = chart_spec->black; + + // blackest, whitest and greyest patches will be found while filling the color values + size_t bwg[3] = { 0, 0, 0 }; + checker->values = _dt_colorchecker_CGATS_fill_patch_values(hIT8, bwg, chart_spec, num_patches); + if(!checker->values) + { + fprintf(stderr, "Error: cannot fill the color values from the CGATS file.\n"); + ERROR + } + + checker->black = bwg[0]; + checker->white = bwg[1]; + checker->middle_grey = bwg[2]; + dt_print(DT_DEBUG_VERBOSE, _("blackest patch: %s, middle grey patch: %s, white patch: %s\n"), + checker->values[bwg[0]].name, checker->values[bwg[1]].name, checker->values[bwg[2]].name); + + dt_print(DT_DEBUG_VERBOSE, _("it8 '%s' done\n"), filename); + goto end; + + error: + fprintf(stderr, "Error creating user ref checker, in %s %s:%d\n", __FUNCTION__, __FILE__, lineno); + + end: + if(!cht_builtin && chart_spec) _dt_color_checker_chart_spec_cleanup(chart_spec); // only allocated chart will be freed + if(hIT8) cmsIT8Free(hIT8); + return checker; +} + +static dt_colorchecker_label_t *_dt_colorchecker_user_ref_add_label(const gchar *filename, const gchar *user_it8_dir) +{ + dt_colorchecker_label_t *result = NULL; + + gchar *filepath = g_build_filename(user_it8_dir, filename, NULL); + if(g_file_test(filepath, G_FILE_TEST_IS_REGULAR)) + { + cmsHANDLE hIT8 = cmsIT8LoadFromFile(NULL, filepath); + + if(hIT8 && _dt_CGATS_is_supported(&hIT8)) + { + gchar *label = _dt_CGATS_get_name(&hIT8, filename); + dt_colorchecker_label_t *CGATS_label = dt_colorchecker_label_init(label, COLOR_CHECKER_USER_REF, filepath); + + g_free(label); + result = CGATS_label; + if(!result) goto error; + } + cmsIT8Free(hIT8); + } + g_free(filepath); + + return result; + +error: + free(result); + return NULL; +} + +static dt_colorchecker_label_t *_dt_colorchecker_cht_add_label(const gchar *filename, const gchar *user_it8_dir) +{ + dt_colorchecker_label_t *result = NULL; + + gchar *filepath = g_build_filename(user_it8_dir, filename, NULL); + if(g_file_test(filepath, G_FILE_TEST_IS_REGULAR)) + { + gchar *basename = g_path_get_basename(filename); + char *dot = g_strrstr(basename, "."); + if(dot) *dot = '\0'; // removes the file extension in basename + + dt_colorchecker_label_t *cht_label = dt_colorchecker_label_init(basename, COLOR_CHECKER_USER_REF, filepath); + + g_free(basename); + + result = cht_label; + if(!result) goto error; + } + g_free(filepath); + + return result; + +error: + free(result); + return NULL; +} + +int dt_colorchecker_find_builtin(GList **colorcheckers_label) +{ + int nb = 0; + for(int k = 0; k < COLOR_CHECKER_USER_REF; k++) + { + const char *name = _dt_get_builtin_colorchecker_name(k); + dt_colorchecker_label_t *builtin_label = dt_colorchecker_label_init(name, k, NULL); + + if(!builtin_label) + { + fprintf(stderr, "Error: failed to allocate memory for builtin colorchecker label %d\n", k); + continue; + } + else + { + *colorcheckers_label = g_list_append(*colorcheckers_label, builtin_label); + nb++; + } + } + return nb; +} + +int dt_colorchecker_find_CGAT_reference_files(GList **ref_colorcheckers_files) +{ + int nb = 0; + char confdir[PATH_MAX] = { 0 }; + dt_loc_get_user_config_dir(confdir, sizeof(confdir)); + gchar *user_it8_dir = g_build_filename(confdir, "color", "it8", NULL); + + GDir *dir = g_dir_open(user_it8_dir, 0, NULL); + if(dir) + { + const char *filename; + while((filename = g_dir_read_name(dir)) != NULL) + { + const char *dot = g_strrstr(filename, "."); + if(g_ascii_strcasecmp(dot, ".cht") == 0) + continue; // skip .cht files + + dt_colorchecker_label_t *CGATS_label = _dt_colorchecker_user_ref_add_label(filename, user_it8_dir); + if(CGATS_label) + { + *ref_colorcheckers_files = g_list_append(*ref_colorcheckers_files, CGATS_label); + nb++; + } + else + fprintf(stderr, "Warning: failed to load CGATS file '%s' in %s\n", filename, user_it8_dir); + } + g_dir_close(dir); + } + g_free(user_it8_dir); + + return nb; +} + +int dt_colorchecker_find_cht_files(GList **chts) +{ + int nb = 0; + char confdir[PATH_MAX] = { 0 }; + dt_loc_get_user_config_dir(confdir, sizeof(confdir)); + gchar *user_it8_dir = g_build_filename(confdir, "color", "it8", NULL); + + GDir *dir = g_dir_open(user_it8_dir, 0, NULL); + if(dir) + { + const char *filename; + while((filename = g_dir_read_name(dir)) != NULL) + { + const char *dot = g_strrstr(filename, "."); + if(g_ascii_strcasecmp(dot, ".cht") != 0) + continue; // skip files that are not .cht + + dt_colorchecker_label_t *cht_label = _dt_colorchecker_cht_add_label(filename, user_it8_dir); + + if(cht_label) + { + *chts = g_list_append(*chts, cht_label); + nb++; + } + } + g_dir_close(dir); + } + g_free(user_it8_dir); + + return nb; +} + +#undef MAX_LINE_LENGTH + +// 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/common/colorchecker.h b/src/common/colorchecker.h index 07638556c59b..b9f91edd7e52 100644 --- a/src/common/colorchecker.h +++ b/src/common/colorchecker.h @@ -1,6 +1,7 @@ /* This file is part of darktable, - Copyright (C) 2020 darktable developers. + Copyright (C) 2020 darktable developers, + Copyright (C) 2025 Guillaume Stutin. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,6 +17,8 @@ along with darktable. If not, see . */ +#include "darktable.h" + /** * These are the CIELab values of Color Checker reference targets */ @@ -25,22 +28,24 @@ typedef enum dt_color_checker_targets { COLOR_CHECKER_XRITE_24_2000 = 0, COLOR_CHECKER_XRITE_24_2014 = 1, - COLOR_CHECKER_SPYDER_24 = 2, - COLOR_CHECKER_SPYDER_24_V2 = 3, - COLOR_CHECKER_SPYDER_48 = 4, - COLOR_CHECKER_SPYDER_48_V2 = 5, + COLOR_CHECKER_SPYDER_24 = 2, + COLOR_CHECKER_SPYDER_24_V2 = 3, + COLOR_CHECKER_SPYDER_48 = 4, + COLOR_CHECKER_SPYDER_48_V2 = 5, + COLOR_CHECKER_USER_REF = 6, COLOR_CHECKER_LAST } dt_color_checker_targets; // helper to deal with patch color typedef struct dt_color_checker_patch { - const char *name; // mnemonic name for the patch - dt_aligned_pixel_t Lab; // reference color in CIE Lab + char *name; // mnemonic name for the patch + dt_aligned_pixel_t Lab; // reference color in CIE Lab - // (x, y) position of the patch center, relatively to the guides (white dots) + // (x, y) position of the patch center, relatively to the guides (white dots) // in ratio of the grid dimension along that axis - struct { + struct + { float x; float y; }; @@ -48,36 +53,23 @@ typedef struct dt_color_checker_patch typedef struct dt_color_checker_t { - const char *name; - const char *author; - const char *date; - const char *manufacturer; + char *name; + char *author; + char *date; + char *manufacturer; dt_color_checker_targets type; - float ratio; // format ratio of the chart, guide to guide (white dots) - float radius; // radius of a patch in ratio of the checker diagonal - size_t patches; // number of patches in target - size_t size[2]; // dimension along x, y axes - size_t middle_grey; // index of the closest patch to 20% neutral grey - size_t white; // index of the closest patch to pure white - size_t black; // index of the closest patch to pure black - dt_color_checker_patch values[]; // array of colors + float ratio; // format ratio of the chart, guide to guide (white dots) + float radius; // radius of a patch in ratio of the checker diagonal + size_t patches; // number of patches in target + size_t size[2]; // dimension along x, y axes + size_t middle_grey; // index of the closest patch to 20% neutral grey + size_t white; // index of the closest patch to pure white + size_t black; // index of the closest patch to pure black + dt_color_checker_patch *values; // pointer to an array of colors } dt_color_checker_t; - -dt_color_checker_t xrite_24_2000 = { .name = "Xrite ColorChecker 24 before 2014", - .author = "X-Rite", - .date = "3/27/2000", - .manufacturer = "X-Rite/Gretag Macbeth", - .type = COLOR_CHECKER_XRITE_24_2000, - .radius = 0.055f, - .ratio = 2.f / 3.f, - .patches = 24, - .size = { 4, 6 }, - .middle_grey = 21, - .white = 18, - .black = 23, - .values = { +dt_color_checker_patch xrite_24_2000_patches[] = { { "A1", { 37.986, 13.555, 14.059 }, { 0.087, 0.125}}, { "A2", { 65.711, 18.13, 17.81 }, { 0.250, 0.125}}, { "A3", { 49.927, -04.88, -21.905 }, { 0.417, 0.125}}, @@ -101,14 +93,13 @@ dt_color_checker_t xrite_24_2000 = { .name = "Xrite ColorChecker 24 before 2014" { "D3", { 66.766, -0.734, -0.504 }, { 0.417, 0.875}}, { "D4", { 50.867, -0.153, -0.27 }, { 0.584, 0.875}}, { "D5", { 35.656, -0.421, -1.231 }, { 0.751, 0.875}}, - { "D6", { 20.461, -0.079, -0.973 }, { 0.918, 0.875}} } }; - + { "D6", { 20.461, -0.079, -0.973 }, { 0.918, 0.875}} }; -dt_color_checker_t xrite_24_2014 = { .name = "Xrite ColorChecker 24 after 2014", +dt_color_checker_t xrite_24_2000 = { .name = "Xrite ColorChecker 24 before 2014", .author = "X-Rite", - .date = "3/28/2015", + .date = "3/27/2000", .manufacturer = "X-Rite/Gretag Macbeth", - .type = COLOR_CHECKER_XRITE_24_2014, + .type = COLOR_CHECKER_XRITE_24_2000, .radius = 0.055f, .ratio = 2.f / 3.f, .patches = 24, @@ -116,7 +107,15 @@ dt_color_checker_t xrite_24_2014 = { .name = "Xrite ColorChecker 24 after 2014", .middle_grey = 21, .white = 18, .black = 23, - .values = { + .values = xrite_24_2000_patches }; + +dt_color_checker_patch xrite_24_2014_patches[] = { + { "A1", { 37.54, 14.37, 14.92 }, { 0.087, 0.125}}, + { "A2", { 64.66, 19.27, 17.50 }, { 0.250, 0.125}}, + { "A3", { 49.32, -03.82, -22.54 }, { 0.417, 0.125}}, + { "A4", { 43.46, -12.74, 22.72 }, { 0.584, 0.125}}, + { "A5", { 54.94, 09.61, -24.79 }, { 0.751, 0.125}}, + { "A6", { 70.48, -32.26, -00.37 }, { 0.918, 0.125}}, { "A1", { 37.54, 14.37, 14.92 }, { 0.087, 0.125}}, { "A2", { 64.66, 19.27, 17.50 }, { 0.250, 0.125}}, { "A3", { 49.32, -03.82, -22.54 }, { 0.417, 0.125}}, @@ -140,26 +139,30 @@ dt_color_checker_t xrite_24_2014 = { .name = "Xrite ColorChecker 24 after 2014", { "D3", { 66.89, -00.75, -00.06 }, { 0.417, 0.875}}, { "D4", { 50.76, -00.13, 00.14 }, { 0.584, 0.875}}, { "D5", { 35.63, -00.46, -00.48 }, { 0.751, 0.875}}, - { "D6", { 20.64, 00.07, -00.46 }, { 0.918, 0.875}} } }; + { "D6", { 20.64, 00.07, -00.46 }, { 0.918, 0.875}} }; + +dt_color_checker_t xrite_24_2014 = { .name = "Xrite ColorChecker 24 after 2014", + .author = "X-Rite", + .date = "3/28/2015", + .manufacturer = "X-Rite/Gretag Macbeth", + .type = COLOR_CHECKER_XRITE_24_2014, + .radius = 0.055f, + .ratio = 2.f / 3.f, + .patches = 24, + .size = { 4, 6 }, + .middle_grey = 21, + .white = 18, + .black = 23, + .values = xrite_24_2014_patches }; // dimensions between reference dots : 197 mm width x 135 mm height // patch width : 26x26 mm // outer gutter : 8 mm // internal gutters (gap between patches) : 5 mm -dt_color_checker_t spyder_24 = { .name = "Datacolor SpyderCheckr 24 before 2018", - .author = "Aur\303\251lien PIERRE", - .date = "dec, 9 2016", - .manufacturer = "DataColor", - .type = COLOR_CHECKER_SPYDER_24, - .ratio = 2.f / 3.f, - .radius = 0.035, - .patches = 24, - .size = { 4, 6 }, - .middle_grey = 03, - .white = 00, - .black = 05, - .values = { { "A1", { 96.04, 2.16, 2.60 }, { 0.107, 0.844 } }, + +dt_color_checker_patch spyder_24_patches[] = { + { "A1", { 96.04, 2.16, 2.60 }, { 0.107, 0.844 } }, { "A2", { 80.44, 1.17, 2.05 }, { 0.264, 0.844 } }, { "A3", { 65.52, 0.69, 1.86 }, { 0.421, 0.844 } }, { "A4", { 49.62, 0.58, 1.56 }, { 0.579, 0.844 } }, @@ -182,13 +185,53 @@ dt_color_checker_t spyder_24 = { .name = "Datacolor SpyderCheckr 24 before 2018 { "D3", { 42.03, -15.80, 22.93 }, { 0.421, 0.155 } }, { "D4", { 48.82, -5.11, -23.08 }, { 0.579, 0.155 } }, { "D5", { 65.10, 18.14, 18.68 }, { 0.736, 0.155 } }, - { "D6", { 36.13, 14.15, 15.78 }, { 0.893, 0.155 } } } }; + { "D6", { 36.13, 14.15, 15.78 }, { 0.893, 0.155 } } }; + +dt_color_checker_t spyder_24 = { .name = "Datacolor SpyderCheckr 24 before 2018", + .author = "Aur\303\251lien PIERRE", + .date = "dec, 9 2016", + .manufacturer = "DataColor", + .type = COLOR_CHECKER_SPYDER_24, + .ratio = 2.f / 3.f, + .radius = 0.035, + .patches = 24, + .size = { 4, 6 }, + .middle_grey = 03, + .white = 00, + .black = 05, + .values = spyder_24_patches }; // dimensions between reference dots : 197 mm width x 135 mm height // patch width : 26x26 mm // outer gutter : 8 mm // internal gutters (gap between patches) : 5 mm + +dt_color_checker_patch spyder_24_v2_patch[] = {{ "A1", { 96.04, 2.16, 2.60 }, { 0.107, 0.844 } }, + { "A2", { 80.44, 1.17, 2.05 }, { 0.264, 0.844 } }, + { "A3", { 65.52, 0.69, 1.86 }, { 0.421, 0.844 } }, + { "A4", { 49.62, 0.58, 1.56 }, { 0.579, 0.844 } }, + { "A5", { 33.55, 0.35, 1.40 }, { 0.736, 0.844 } }, + { "A6", { 16.91, 1.43, -0.81 }, { 0.893, 0.844 } }, + { "B1", { 47.12, -32.50, -28.75 }, { 0.107, 0.615 } }, + { "B2", { 50.49, 53.45, -13.55 }, { 0.264, 0.615 } }, + { "B3", { 83.61, 3.36, 87.02 }, { 0.421, 0.615 } }, + { "B4", { 41.05, 60.75, 31.17 }, { 0.579, 0.615 } }, + { "B5", { 54.14, -40.80, 34.75 }, { 0.736, 0.615 } }, + { "B6", { 24.75, 13.78, -49.48 }, { 0.893, 0.615 } }, + { "C1", { 60.94, 38.21, 61.31 }, { 0.107, 0.385 } }, + { "C2", { 37.80, 7.30, -43.04 }, { 0.264, 0.385 } }, + { "C3", { 49.81, 48.50, 15.76 }, { 0.421, 0.385 } }, + { "C4", { 28.88, 19.36, -24.48 }, { 0.579, 0.385 } }, + { "C5", { 72.45, -23.57, 60.47 }, { 0.736, 0.385 } }, + { "C6", { 71.65, 23.74, 72.28 }, { 0.893, 0.385 } }, + { "D1", { 70.19, -31.85, 1.98 }, { 0.107, 0.155 } }, + { "D2", { 54.38, 8.84, -25.71 }, { 0.264, 0.155 } }, + { "D3", { 42.03, -15.78, 22.93 }, { 0.421, 0.155 } }, + { "D4", { 48.82, -5.11, -23.08 }, { 0.579, 0.155 } }, + { "D5", { 65.10, 18.14, 18.68 }, { 0.736, 0.155 } }, + { "D6", { 36.13, 14.15, 15.78 }, { 0.893, 0.155 } } }; + dt_color_checker_t spyder_24_v2 = { .name = "Datacolor SpyderCheckr 24 after 2018", .author = "Aur\303\251lien PIERRE", .date = "dec, 9 2016", @@ -201,49 +244,15 @@ dt_color_checker_t spyder_24_v2 = { .name = "Datacolor SpyderCheckr 24 after 20 .middle_grey = 03, .white = 00, .black = 05, - .values = { { "A1", { 96.04, 2.16, 2.60 }, { 0.107, 0.844 } }, - { "A2", { 80.44, 1.17, 2.05 }, { 0.264, 0.844 } }, - { "A3", { 65.52, 0.69, 1.86 }, { 0.421, 0.844 } }, - { "A4", { 49.62, 0.58, 1.56 }, { 0.579, 0.844 } }, - { "A5", { 33.55, 0.35, 1.40 }, { 0.736, 0.844 } }, - { "A6", { 16.91, 1.43, -0.81 }, { 0.893, 0.844 } }, - { "B1", { 47.12, -32.50, -28.75 }, { 0.107, 0.615 } }, - { "B2", { 50.49, 53.45, -13.55 }, { 0.264, 0.615 } }, - { "B3", { 83.61, 3.36, 87.02 }, { 0.421, 0.615 } }, - { "B4", { 41.05, 60.75, 31.17 }, { 0.579, 0.615 } }, - { "B5", { 54.14, -40.80, 34.75 }, { 0.736, 0.615 } }, - { "B6", { 24.75, 13.78, -49.48 }, { 0.893, 0.615 } }, - { "C1", { 60.94, 38.21, 61.31 }, { 0.107, 0.385 } }, - { "C2", { 37.80, 7.30, -43.04 }, { 0.264, 0.385 } }, - { "C3", { 49.81, 48.50, 15.76 }, { 0.421, 0.385 } }, - { "C4", { 28.88, 19.36, -24.48 }, { 0.579, 0.385 } }, - { "C5", { 72.45, -23.57, 60.47 }, { 0.736, 0.385 } }, - { "C6", { 71.65, 23.74, 72.28 }, { 0.893, 0.385 } }, - { "D1", { 70.19, -31.85, 1.98 }, { 0.107, 0.155 } }, - { "D2", { 54.38, 8.84, -25.71 }, { 0.264, 0.155 } }, - { "D3", { 42.03, -15.78, 22.93 }, { 0.421, 0.155 } }, - { "D4", { 48.82, -5.11, -23.08 }, { 0.579, 0.155 } }, - { "D5", { 65.10, 18.14, 18.68 }, { 0.736, 0.155 } }, - { "D6", { 36.13, 14.15, 15.78 }, { 0.893, 0.155 } } } }; + .values = spyder_24_v2_patch }; // dimensions between reference dots : 297 mm width x 197 mm height // patch width : 26x26 mm // outer gutter : 8 mm // internal gutters (gap between patches) : 5 mm -dt_color_checker_t spyder_48 = { .name = "Datacolor SpyderCheckr 48 before 2018", - .author = "Aur\303\251lien PIERRE", - .date = "dec, 9 2016", - .manufacturer = "DataColor", - .type = COLOR_CHECKER_SPYDER_48, - .ratio = 2.f / 3.f, - .radius = 0.035, - .patches = 48, - .size = { 8, 6 }, - .middle_grey = 24, - .white = 21, - .black = 29, - .values = { { "A1", { 61.35, 34.81, 18.38 }, { 0.071, 0.107 } }, + +dt_color_checker_patch spyder_48_patches[] = { { "A1", { 61.35, 34.81, 18.38 }, { 0.071, 0.107 } }, { "A2", { 75.50 , 5.84, 50.42 }, { 0.071, 0.264 } }, { "A3", { 66.82, -25.1, 23.47 }, { 0.071, 0.421 } }, { "A4", { 60.53, -22.6, -20.40 }, { 0.071, 0.579 } }, @@ -290,18 +299,13 @@ dt_color_checker_t spyder_48 = { .name = "Datacolor SpyderCheckr 48 before 2018 { "H3", { 42.03, -15.80, 22.93 }, { 0.929, 0.421 } }, { "H4", { 48.82, -5.11, -23.08 }, { 0.929, 0.579 } }, { "H5", { 65.10, 18.14, 18.68 }, { 0.929, 0.736 } }, - { "H6", { 36.13, 14.15, 15.78 }, { 0.929, 0.893 } } } }; + { "H6", { 36.13, 14.15, 15.78 }, { 0.929, 0.893 } } }; - -// dimensions between reference dots : 297 mm width x 197 mm height -// patch width : 26x26 mm -// outer gutter : 8 mm -// internal gutters (gap between patches) : 5 mm -dt_color_checker_t spyder_48_v2 = { .name = "Datacolor SpyderCheckr 48 after 2018", +dt_color_checker_t spyder_48 = { .name = "Datacolor SpyderCheckr 48 before 2018", .author = "Aur\303\251lien PIERRE", .date = "dec, 9 2016", .manufacturer = "DataColor", - .type = COLOR_CHECKER_SPYDER_48_V2, + .type = COLOR_CHECKER_SPYDER_48, .ratio = 2.f / 3.f, .radius = 0.035, .patches = 48, @@ -309,7 +313,15 @@ dt_color_checker_t spyder_48_v2 = { .name = "Datacolor SpyderCheckr 48 after 20 .middle_grey = 24, .white = 21, .black = 29, - .values = { { "A1", { 61.35, 34.81, 18.38 }, { 0.071, 0.107 } }, + .values = spyder_48_patches }; + + +// dimensions between reference dots : 297 mm width x 197 mm height +// patch width : 26x26 mm +// outer gutter : 8 mm +// internal gutters (gap between patches) : 5 mm + +dt_color_checker_patch spyder_48_v2_patch[] = { { "A1", { 61.35, 34.81, 18.38 }, { 0.071, 0.107 } }, { "A2", { 75.50 , 5.84, 50.42 }, { 0.071, 0.264 } }, { "A3", { 66.82, -25.1, 23.47 }, { 0.071, 0.421 } }, { "A4", { 60.53, -22.62, -20.40 }, { 0.071, 0.579 } }, @@ -356,36 +368,317 @@ dt_color_checker_t spyder_48_v2 = { .name = "Datacolor SpyderCheckr 48 after 20 { "H3", { 42.03, -15.78, 22.93 }, { 0.929, 0.421 } }, { "H4", { 48.82, -5.11, -23.08 }, { 0.929, 0.579 } }, { "H5", { 65.10, 18.14, 18.68 }, { 0.929, 0.736 } }, - { "H6", { 36.13, 14.15, 15.78 }, { 0.929, 0.893 } } } }; + { "H6", { 36.13, 14.15, 15.78 }, { 0.929, 0.893 } } }; + +dt_color_checker_t spyder_48_v2 = { .name = "Datacolor SpyderCheckr 48 after 2018", + .author = "Aur\303\251lien PIERRE", + .date = "dec, 9 2016", + .manufacturer = "DataColor", + .type = COLOR_CHECKER_SPYDER_48_V2, + .ratio = 2.f / 3.f, + .radius = 0.035, + .patches = 48, + .size = { 8, 6 }, + .middle_grey = 24, + .white = 21, + .black = 29, + .values = spyder_48_v2_patch }; + +typedef struct dt_colorchecker_label_t +{ + gchar *label; + dt_color_checker_targets type; + gchar *path; +} dt_colorchecker_label_t; +// Add other supported type of CGATS here +typedef enum dt_colorchecker_CGATS_types +{ + CGATS_TYPE_IT8_7_1, + CGATS_TYPE_IT8_7_2, + CGATS_TYPE_UNKOWN +} dt_colorchecker_CGATS_types; + +const char *CGATS_types[CGATS_TYPE_UNKOWN] = { + "IT8.7/1", // transparent + "IT8.7/2" // opaque +}; + +typedef enum dt_colorchecker_material_types +{ + COLOR_CHECKER_MATERIAL_TRANSPARENT, + COLOR_CHECKER_MATERIAL_OPAQUE, + COLOR_CHECKER_MATERIAL_UNKNOWN +} dt_colorchecker_material_types; -dt_color_checker_t * dt_get_color_checker(const dt_color_checker_targets target_type) +const char *colorchecker_material_types[COLOR_CHECKER_MATERIAL_UNKNOWN] = { + "Transparent", + "Opaque" +}; + +typedef struct dt_colorchecker_CGATS_label_name_t +{ + const char *type; + const char *originator; + const char *date; // date in format 'Mon YYYY' + const char *material; +} dt_colorchecker_CGATS_label_name_t; + +// This defines charts specifications +typedef struct dt_colorchecker_chart_spec_t +{ + const gchar *type; + float radius; // radius of a patch in ratio of the checker diagonal + float ratio; // format ratio of the chart, guide to guide (white dots) + size_t size[2]; // number of patch along x, y axes + float guide_size[2];// size of the guide area, specified by "MARK" data in cht files + size_t middle_grey; + size_t white; + size_t black; + + int num_patches; // total number of patches + int colums; + int rows; + float patch_width; + float patch_height; + float patch_offset_x; + float patch_offset_y; + + GSList *patches; // list of patches struct, data are dt_color_checker_patch + + // pointer to the buildin chart definition. + // If set, the struct should not be freed because not allocated. + void *address; + +} dt_colorchecker_chart_spec_t; + +dt_colorchecker_chart_spec_t IT8_7 = { + .type = "IT8", + .radius = 0.0189f, + .ratio = 6.f / 11.f, + .size = { 22, 13 }, + .guide_size = { 0, 0 }, + .middle_grey = 273, // GS09 + .white = 263, // Dmin or GS00 + .black = 287, // Dmax or GS23 + + .num_patches = 288, // as specified in IT8.7/1 and IT8.7/2 + .colums = 22, + .rows = 12, + .patch_width = 0.04255f, // 1.0f / (cols + 1.5f) + .patch_height = 0.0740f, // 1.0f / (rows + 1.5f) + .patch_offset_x = 0.0531f, // 1.25f * patch_size_x + .patch_offset_y = 0.0925f, // 1.25f * patch_size_y + .address = &IT8_7 +}; + +dt_colorchecker_label_t *dt_colorchecker_label_init(const char *label, const dt_color_checker_targets type, const char *path) { - switch(target_type) + size_t label_size = safe_strlen(label) + safe_strlen(path) + sizeof(dt_color_checker_targets); + + dt_colorchecker_label_t *checker_label = malloc(label_size); + if(!checker_label) return NULL; + + checker_label->label = g_strdup(label); + checker_label->type = type; + checker_label->path = path ? g_strdup(path) : NULL; + + return checker_label; +} + +dt_color_checker_patch *dt_color_checker_patch_array_init(const size_t num_patches) +{ + dt_color_checker_patch *patches = (dt_color_checker_patch *)dt_alloc_align(num_patches * sizeof(dt_color_checker_patch)); + if(!patches) return NULL; + + // Initialize the patches + for(size_t i = 0; i < num_patches; i++) { - case COLOR_CHECKER_XRITE_24_2000: - return &xrite_24_2000; + patches[i].name = NULL; + patches[i].x = 0.0f; + patches[i].y = 0.0f; + patches[i].Lab[0] = 0.0f; + patches[i].Lab[1] = 0.0f; + patches[i].Lab[2] = 0.0f; + } + return patches; +} - case COLOR_CHECKER_XRITE_24_2014: - return &xrite_24_2014; +void dt_color_checker_patch_cleanup(const dt_color_checker_patch *patch) +{ + if(!patch) return; + if(!patch->name) return; - case COLOR_CHECKER_SPYDER_24: - return &spyder_24; + g_free(patch->name); +} - case COLOR_CHECKER_SPYDER_24_V2: - return &spyder_24_v2; +// This one is to fully free GSList of dt_color_checker_patch +void dt_color_checker_patch_cleanup_list(void *_patch) +{ + dt_color_checker_patch *patch = (dt_color_checker_patch *)_patch; + if(!patch) return; - case COLOR_CHECKER_SPYDER_48: - return &spyder_48; + // Free the name if it was allocated + if(patch->name) g_free(patch->name); - case COLOR_CHECKER_SPYDER_48_V2: - return &spyder_48_v2; + free(patch); +} + +dt_color_checker_t *dt_colorchecker_init() +{ + dt_color_checker_t *checker = (dt_color_checker_t*)malloc(sizeof(dt_color_checker_t)); + + return checker ? checker : NULL; +} + +void dt_color_checker_cleanup(dt_color_checker_t *checker) +{ + if (!checker) return; + + if(checker->name) g_free(checker->name); + if(checker->author) g_free(checker->author); + if(checker->date) g_free(checker->date); + if(checker->manufacturer) g_free(checker->manufacturer); + + if(checker->patches > 0 && checker->values) + { + for(size_t i = 0; i < checker->patches; i++) + { + dt_color_checker_patch_cleanup(&checker->values[i]); + } + + dt_free_align(checker->values); + } + free(checker); + checker = NULL; +} + +void dt_colorchecker_label_free(gpointer data) +{ + dt_colorchecker_label_t *checker_label = (dt_colorchecker_label_t *)data; + + if(!checker_label) return; + + // Only free if not NULL and if dynamically allocated (user reference) + if(checker_label->type == COLOR_CHECKER_USER_REF) + { + g_free(checker_label->label); + if(checker_label->path) g_free(checker_label->path); // Builtin checker doesn't use this char dynamically + } +} + +void dt_colorchecker_label_list_cleanup(GList **colorcheckers) +{ + if(!colorcheckers) return; + + g_list_free_full(g_steal_pointer(colorcheckers), dt_colorchecker_label_free); + *colorcheckers = NULL; +} + +void dt_colorchecker_cht_list_cleanup(GList **cht) +{ + if(!cht) return; + + g_list_free_full(g_steal_pointer(cht), dt_colorchecker_label_free); + *cht = NULL; +} +/** + * @brief Creates a color checker from a user reference file (CGATS format). + * + * @param filename the path to the CGATS file. + * @param cht_filename the path to the .cht file (optional, can be NULL). + * @return dt_color_checker_t* the filled color checker. + */ +dt_color_checker_t *dt_colorchecker_user_ref_create(const char *filename, const char *cht_filename); + +/** + * @brief Find all .cht files in the user config/color/it8 directory + * + * @param chts NULL GList that will be populated with found IT8 files + * @return int Number of found files + */ +int dt_colorchecker_find_cht_files(GList **chts); + +/** + * @brief Find all CGAT files in the user config/color/it8 directory + * + * @param ref_colorcheckers_files NULL GList that will be populated with found IT8 files + * @return int Number of found files + */ +int dt_colorchecker_find_CGAT_reference_files(GList **ref_colorcheckers_files); + +int dt_colorchecker_find_builtin(GList **colorcheckers_label); + +/** + * @brief Copy the content of a color checker from source to destination. + * + * @param dest A pointer to the destination color checker. + * @param src A pointer to the source color checker. + */ +void dt_color_checker_copy(dt_color_checker_t *dest, const dt_color_checker_t *src); + + +static dt_color_checker_t *dt_get_color_checker(const dt_color_checker_targets target_type, GList **colorchecker_label, const char *cht_filename) +{ + dt_color_checker_t *checker_dest = NULL; + // cleanup and initialize the destination checker + checker_dest = dt_colorchecker_init(); + if(!checker_dest) return NULL; + + // check if the target type is a user reference + dt_color_checker_targets nth_checker = COLOR_CHECKER_LAST; + const dt_colorchecker_label_t *label_data = NULL; + if(target_type >= COLOR_CHECKER_USER_REF && colorchecker_label != NULL && *colorchecker_label != NULL) + { + dt_print(DT_DEBUG_VERBOSE, _("dt_get_color_checker: colorchecker type %i is a user reference.\n"), target_type); + + // Get the label data from the list + label_data = g_list_nth_data(*colorchecker_label, target_type); + nth_checker = COLOR_CHECKER_USER_REF; + } + else // it's a builtin colorchecker + nth_checker = target_type; + + // Copy the color checker data from the predefined checkers + switch(nth_checker) + { + case COLOR_CHECKER_XRITE_24_2000: + dt_color_checker_copy(checker_dest, &xrite_24_2000); + break; + case COLOR_CHECKER_XRITE_24_2014: + dt_color_checker_copy(checker_dest, &xrite_24_2014); + break; + case COLOR_CHECKER_SPYDER_24: + dt_color_checker_copy(checker_dest, &spyder_24); + break; + case COLOR_CHECKER_SPYDER_24_V2: + dt_color_checker_copy(checker_dest, &spyder_24_v2); + break; + case COLOR_CHECKER_SPYDER_48: + dt_color_checker_copy(checker_dest, &spyder_48); + break; + case COLOR_CHECKER_SPYDER_48_V2: + dt_color_checker_copy(checker_dest, &spyder_48_v2); + break; + case COLOR_CHECKER_USER_REF: + if(label_data) + { + dt_color_checker_t *p = dt_colorchecker_user_ref_create(label_data->path, cht_filename); + if(p) + { + dt_color_checker_copy(checker_dest, p); + dt_color_checker_cleanup(p); + } + } + break; + case COLOR_CHECKER_LAST: - return &xrite_24_2014; + fprintf(stderr, "dt_get_color_checker: colorchecker type %i not found!\n", target_type); + dt_color_checker_copy(checker_dest, &xrite_24_2014); } - return &xrite_24_2014; + return checker_dest; } /** @@ -413,8 +706,7 @@ static inline void dt_color_checker_get_coordinates(const dt_color_checker_t *co } // find a patch matching a name -static inline const dt_color_checker_patch* dt_color_checker_get_patch_by_name(const dt_color_checker_t *const target_checker, - const char *name, size_t *index) +static inline const dt_color_checker_patch *dt_color_checker_get_patch_by_name(const dt_color_checker_t *const target_checker, const char *name, size_t *index) { size_t idx = -1; const dt_color_checker_patch *patch = NULL; @@ -429,9 +721,44 @@ static inline const dt_color_checker_patch* dt_color_checker_get_patch_by_name(c if(patch == NULL) fprintf(stderr, "No patch matching name `%s` was found in %s\n", name, target_checker->name); - if(index ) *index = idx; + if(index) *index = idx; return patch; } + +/** + * @brief Find all builtin and CGATS colorcheckers. + * + * @param colorcheckers_label the NULL GList that will be populated with found colorcheckers. + * @return int Number of found colorcheckers. + */ +int dt_colorchecker_find(GList **colorcheckers_label) +{ + int total = dt_colorchecker_find_builtin(colorcheckers_label); + dt_print(DT_DEBUG_VERBOSE, _("dt_colorchecker_find: found %d builtin colorcheckers\n"), total); + int b_nb = total; + + total += dt_colorchecker_find_CGAT_reference_files(colorcheckers_label); + if (total) dt_print(DT_DEBUG_VERBOSE, _("dt_colorchecker_find: found %d CGAT references files\n"), total - b_nb); + return total; +} + +/** + * @brief Find all .cht files in the user it8 directory. + * + * @param cht A NULL GList that will be populated with found .cht files. + * @return int The number of found .cht files. + */ +int dt_colorchecker_find_cht(GList **cht) +{ + if(!cht) return 0; + + const int total = dt_colorchecker_find_cht_files(cht); + + if(total) dt_print(DT_DEBUG_VERBOSE, _("dt_colorchecker_find_cht: found %d .cht files\n"), total); + + return total; +} + // 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 diff --git a/src/iop/channelmixerrgb.c b/src/iop/channelmixerrgb.c index 9cdd3a4089d9..047b29066312 100644 --- a/src/iop/channelmixerrgb.c +++ b/src/iop/channelmixerrgb.c @@ -1,6 +1,8 @@ /* This file is part of darktable, - Copyright (C) 2010-2022 darktable developers. + Copyright (C) 2010-2022 darktable developers, + Copyright (C) 2025 Guillaume Stutin. + darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,27 +23,28 @@ #endif #include "bauhaus/bauhaus.h" #include "chart/common.h" -#include "develop/imageop_gui.h" -#include "dtgtk/drawingarea.h" #include "common/chromatic_adaptation.h" #include "common/colorspaces_inline_conversions.h" #include "common/colorchecker.h" -#include "common/opencl.h" +#include "common/file_location.h" #include "common/illuminants.h" #include "common/imagebuf.h" #include "common/iop_profile.h" +#include "common/opencl.h" #include "control/control.h" +#include "develop/imageop_gui.h" #include "develop/imageop_math.h" #include "develop/openmp_maths.h" - +#include "dtgtk/drawingarea.h" +#include "gaussian_elimination.h" #include "gui/color_picker_proxy.h" #include "gui/gtk.h" #include "gui/presets.h" #include "iop/iop_api.h" -#include "gaussian_elimination.h" #include #include +#include #include #include #include @@ -170,8 +173,14 @@ typedef struct dt_iop_channelmixer_rgb_gui_data_t gboolean checker_ready; // notify that a checker bounding box is ready to be used dt_colormatrix_t mix; + GList *colorcheckers; + int n_colorcheckers; + + GList *colorcheckers_cht; // list of cht files + int n_cht; + gboolean is_profiling_started; - GtkWidget *checkers_list, *optimize, *safety, *label_delta_E, *button_profile, *button_validate, *button_commit; + GtkWidget *checkers_list, *checkers_cht_list, *optimize, *safety, *label_delta_E, *button_profile, *button_validate, *button_commit; float *delta_E_in; @@ -1632,7 +1641,7 @@ void extract_color_checker(const float *const restrict in, float *const restrict dt_aligned_pixel_t LMS_test; convert_any_XYZ_to_LMS(sample, LMS_test, kind); - float *const reference = g->checker->values[k].Lab; + const float *const reference = g->checker->values[k].Lab; dt_aligned_pixel_t XYZ_ref, LMS_ref; dt_Lab_to_XYZ(reference, XYZ_ref); convert_any_XYZ_to_LMS(XYZ_ref, LMS_ref, kind); @@ -2430,6 +2439,22 @@ void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, i cairo_arc(cr, 0.5 * wd, 0.5 * ht, 7., 0, 2. * M_PI); cairo_stroke(cr); */ + if(!g->checker->values) + { + const point_t target_center = { 0.5f, 0.5f }; + point_t new_target_center = apply_homography(target_center, g->homography); + + cairo_set_source_rgba(cr, 1., 0., 0., 1.); + cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); + cairo_set_font_size(cr, 40.0 / zoom_scale); + + cairo_text_extents_t extents; + cairo_text_extents(cr, "Error", &extents); + + cairo_move_to(cr, new_target_center.x - extents.width / 2.0, new_target_center.y + extents.height / 2.0); + cairo_show_text(cr, "Error"); + return; + } const float radius_x = g->checker->radius * hypotf(1.f, g->checker->ratio) * g->safety_margin; const float radius_y = radius_x / g->checker->ratio; @@ -2511,7 +2536,48 @@ static void optimize_changed_callback(GtkWidget *widget, gpointer user_data) dt_iop_gui_leave_critical_section(self); } -static void checker_changed_callback(GtkWidget *widget, gpointer user_data) +void cht_list_visibility(dt_iop_module_t *self, const int i) +{ + dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data; + + if( i >= COLOR_CHECKER_USER_REF) + gtk_widget_show(GTK_WIDGET(g->checkers_cht_list)); + + else + gtk_widget_hide(GTK_WIDGET(g->checkers_cht_list)); +} + +static void checker_cht_changed_callback(GtkWidget *widget, gpointer user_data) +{ + if(darktable.gui->reset) return; + dt_iop_module_t *self = (dt_iop_module_t *)user_data; + dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data; + + const int i = dt_bauhaus_combobox_get(widget); + dt_conf_set_int("darkroom/modules/channelmixerrgb/colorchecker_cht", i); + + const int n_chkr = dt_bauhaus_combobox_get(g->checkers_list); + + const dt_colorchecker_label_t *cht_label = (i >= 0 && g->colorcheckers_cht) ? g_list_nth_data(g->colorcheckers_cht, i - 1) : NULL; + const char *cht_path = cht_label ? cht_label->path : NULL; + + dt_color_checker_cleanup(g->checker); + g->checker = dt_get_color_checker(n_chkr, &(g->colorcheckers), cht_path); + + dt_develop_t *dev = self->dev; + const float wd = dev->preview_pipe->backbuf_width; + const float ht = dev->preview_pipe->backbuf_height; + if(wd == 0.f || ht == 0.f) return; + + dt_iop_gui_enter_critical_section(self); + g->profile_ready = FALSE; + init_bounding_box(g, wd, ht); + dt_iop_gui_leave_critical_section(self); + + dt_control_queue_redraw_center(); +} + +void checker_changed_callback(GtkWidget *widget, gpointer user_data) { if(darktable.gui->reset) return; dt_iop_module_t *self = (dt_iop_module_t *)user_data; @@ -2519,7 +2585,13 @@ static void checker_changed_callback(GtkWidget *widget, gpointer user_data) const int i = dt_bauhaus_combobox_get(widget); dt_conf_set_int("darkroom/modules/channelmixerrgb/colorchecker", i); - g->checker = dt_get_color_checker(i); + const int n_cht = dt_bauhaus_combobox_get(g->checkers_cht_list) - 1; + const dt_colorchecker_label_t *cht_label = (n_cht >= 0 && g->colorcheckers_cht) ? g_list_nth_data(g->colorcheckers_cht, n_cht) : NULL; + const char *cht_path = cht_label ? cht_label->path : NULL; + dt_color_checker_cleanup(g->checker); + g->checker = dt_get_color_checker(i, &(g->colorcheckers), cht_path); + + cht_list_visibility(self, i); dt_develop_t *dev = self->dev; const float wd = dev->preview_pipe->backbuf_width; @@ -3337,6 +3409,63 @@ static void illum_xy_callback(GtkWidget *slider, gpointer user_data) dt_dev_add_history_item(darktable.develop, self, TRUE); } +void update_colorchecker_cht_list(dt_iop_module_t *self) +{ + dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data; + + if(!g) return; + + // clear and refill the chart definition list + dt_colorchecker_cht_list_cleanup(&(g->colorcheckers_cht)); + + g->n_cht = 0; + int pos = -1; + + pos += dt_colorchecker_find_cht(&(g->colorcheckers_cht)); + + g->n_cht = pos; + + // update the gui + dt_bauhaus_combobox_clear(g->checkers_cht_list); + dt_bauhaus_combobox_add(g->checkers_cht_list, _("Standard")); + + for(GList *l = g_list_first(g->colorcheckers_cht); l; l = g_list_next(l)) + { + const dt_colorchecker_label_t *cht_data = (dt_colorchecker_label_t *)l->data; + const char *cht_name = cht_data->label; + dt_bauhaus_combobox_add(g->checkers_cht_list, cht_name); + } + +} + +void update_colorchecker_list(dt_iop_module_t *self) +{ + dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data; + + if(!g) return; + + // clear and refill the colorchecker list + dt_colorchecker_label_list_cleanup(&(g->colorcheckers)); + + g->n_colorcheckers = 0; + + int pos = -1; + + pos += dt_colorchecker_find(&(g->colorcheckers)); + + g->n_colorcheckers = pos; + + // update the gui + dt_bauhaus_combobox_clear(g->checkers_list); + + for(GList *l = g_list_first(g->colorcheckers); l; l = g_list_next(l)) + { + const dt_colorchecker_label_t *checker_data = (dt_colorchecker_label_t *)l->data; + const char *checkername = checker_data->label; + dt_bauhaus_combobox_add(g->checkers_list, checkername); + } +} + void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = dt_calloc_align(sizeof(dt_iop_channelmixer_rbg_data_t)); @@ -3410,9 +3539,21 @@ void gui_update(struct dt_iop_module_t *self) dt_iop_gui_enter_critical_section(self); + update_colorchecker_list(self); + update_colorchecker_cht_list(self); + const int i = dt_conf_get_int("darkroom/modules/channelmixerrgb/colorchecker"); dt_bauhaus_combobox_set(g->checkers_list, i); - g->checker = dt_get_color_checker(i); + + const int cht = dt_conf_get_int("darkroom/modules/channelmixerrgb/colorchecker_cht"); + dt_bauhaus_combobox_set(g->checkers_cht_list, cht); + + const dt_colorchecker_label_t *label = (cht > 0) && g->colorcheckers_cht ? g_list_nth_data(g->colorcheckers_cht, cht - 1) : NULL; + const char *cht_path = label ? label->path : NULL; + dt_color_checker_cleanup(g->checker); + g->checker = dt_get_color_checker(i, &(g->colorcheckers), cht_path); + + cht_list_visibility(self, i); const int j = dt_conf_get_int("darkroom/modules/channelmixerrgb/optimization"); dt_bauhaus_combobox_set(g->optimize, j); @@ -3948,6 +4089,7 @@ void gui_init(struct dt_iop_module_t *self) g->checker_ready = FALSE; g->delta_E_in = NULL; g->delta_E_label_text = NULL; + g->colorcheckers = NULL; g->XYZ[0] = NAN; @@ -4162,17 +4304,44 @@ void gui_init(struct dt_iop_module_t *self) GtkWidget *collapsible = GTK_WIDGET(g->cs.container); - DT_BAUHAUS_COMBOBOX_NEW_FULL(darktable.bauhaus, g->checkers_list, DT_GUI_MODULE(self), N_("chart"), - _("choose the vendor and the type of your chart"), - 0, checker_changed_callback, self, - N_("Xrite ColorChecker 24 pre-2014"), - N_("Xrite ColorChecker 24 post-2014"), - N_("Datacolor SpyderCheckr 24 pre-2018"), - N_("Datacolor SpyderCheckr 24 post-2018"), - N_("Datacolor SpyderCheckr 48 pre-2018"), - N_("Datacolor SpyderCheckr 48 post-2018")); - gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_list), TRUE, TRUE, 0); + gchar *tip_files_loc = NULL; + { + char datadir[PATH_MAX] = { 0 }; + char confdir[PATH_MAX] = { 0 }; + dt_loc_get_datadir(datadir, sizeof(datadir)); + dt_loc_get_user_config_dir(confdir, sizeof(confdir)); + + gchar *system_CGATS_dir = g_build_filename(datadir, "color", "it8", NULL); + gchar *user_CGATS_dir = g_build_filename(confdir, "color", "it8", NULL); + tip_files_loc = g_strdup_printf(_("files must be placed in %s or %s"), + system_CGATS_dir, user_CGATS_dir); + + g_free(system_CGATS_dir); + g_free(user_CGATS_dir); + } + g->checkers_list = dt_bauhaus_combobox_new(darktable.bauhaus, DT_GUI_MODULE(self)); + dt_bauhaus_widget_set_label(g->checkers_list, N_("Chart")); + gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_list), TRUE, TRUE, 0); + dt_bauhaus_combobox_set(g->checkers_list, 0); + gchar *tooltip = g_strdup_printf(_("Choose the vendor and the type of your chart.\n" + "CGATS.17 references %s."), tip_files_loc); + gtk_widget_set_tooltip_text(g->checkers_list, tooltip); + g_free(tooltip); + g_signal_connect(G_OBJECT(g->checkers_list), "value-changed", G_CALLBACK(checker_changed_callback), (gpointer)self); + + g->checkers_cht_list = dt_bauhaus_combobox_new(darktable.bauhaus, DT_GUI_MODULE(self)); + dt_bauhaus_widget_set_label(g->checkers_cht_list, N_("Chart geometry")); + gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_cht_list), TRUE, TRUE, 0); + + dt_bauhaus_combobox_set(g->checkers_cht_list, 0); + tooltip = g_strdup_printf(_("Choose the definition of your chart.\n" + ".cht references %s."), tip_files_loc); + gtk_widget_set_tooltip_text(g->checkers_cht_list, tooltip); + g_free(tip_files_loc); + g_free(tooltip); + g_signal_connect(G_OBJECT(g->checkers_cht_list), "value-changed", G_CALLBACK(checker_cht_changed_callback), (gpointer)self); + DT_BAUHAUS_COMBOBOX_NEW_FULL(darktable.bauhaus, g->optimize, DT_GUI_MODULE(self), N_("optimize for"), _("choose the colors that will be optimized with higher priority.\n" "neutral colors gives the lowest average delta E but a high maximum delta E\n" @@ -4240,6 +4409,9 @@ void gui_cleanup(struct dt_iop_module_t *self) g_free(g->delta_E_label_text); + dt_colorchecker_label_list_cleanup(&(g->colorcheckers)); + dt_color_checker_cleanup(g->checker); + IOP_GUI_FREE; }