From 77840615ceb64ff00f950c3ae627a0263ca29f73 Mon Sep 17 00:00:00 2001 From: "Benoit Brummer (Trougnouf)" Date: Tue, 7 Jul 2026 10:43:48 +0200 Subject: [PATCH] Add support for embedded lens corrections in RawNIND/AI-denoised DNGs Implement serialization of DNG OpcodeList2 and OpcodeList3 to preserve embedded lens correction data (warp rectilinear, vignette radial, gain maps) when writing DNG files. Key changes: - Add dt_dng_opcode_serialize_opcode_list_2() and _3() functions to serialize correction data from dt_image_t to DNG opcode list format - Add _write_dng_opcode_lists() helper in imageio_dng.c to write opcode lists to TIFF directories using libtiff's TIFFTAG_OPCODELIST2/3 tags - Modify dt_imageio_dng_write_cfa_bayer() and dt_imageio_dng_write_linear() to call _write_dng_opcode_lists() and preserve correction data - Add fallback definitions for TIFFTAG_OPCODELIST* tags for older libtiff This ensures that when RawNIND/AI-denoise creates a DNG file from a source image that has embedded lens corrections (e.g., from a DNG with OpcodeList3), those corrections are preserved in the output DNG and will be automatically applied when the denoised DNG is re-imported into darktable. Fixes: #21504 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- src/common/dng_opcode.c | 220 ++++++++++++++++++++++++++++++++++++++ src/common/dng_opcode.h | 4 + src/imageio/imageio_dng.c | 40 +++++++ src/imageio/imageio_dng.h | 1 + 4 files changed, 265 insertions(+) diff --git a/src/common/dng_opcode.c b/src/common/dng_opcode.c index 153b6d8c9776..10989e41d59a 100644 --- a/src/common/dng_opcode.c +++ b/src/common/dng_opcode.c @@ -18,6 +18,7 @@ #include #include +#include #include "debug.h" #include "dng_opcode.h" @@ -57,6 +58,31 @@ static uint32_t _get_long(uint8_t *ptr) return GUINT32_FROM_BE(in); } + +static void _put_long(uint8_t *ptr, uint32_t val) +{ + uint32_t out = GUINT32_TO_BE(val); + memcpy(ptr, &out, sizeof(out)); +} + +static void _put_double(uint8_t *ptr, double val) +{ + guint64 out; + union { guint64 in; double v; } u; + u.v = val; + out = GUINT64_TO_BE(u.in); + memcpy(ptr, &out, sizeof(out)); +} + +static void _put_float(uint8_t *ptr, float val) +{ + guint32 out; + union { guint32 in; float v; } u; + u.v = val; + out = GUINT32_TO_BE(u.in); + memcpy(ptr, &out, sizeof(out)); +} + void dt_dng_opcode_process_opcode_list_2(uint8_t *buf, uint32_t buf_size, dt_image_t *img) { g_list_free_full(img->dng_gain_maps, g_free); @@ -177,3 +203,197 @@ void dt_dng_opcode_process_opcode_list_3(uint8_t *buf, uint32_t buf_size, dt_ima count--; } } + + +// Serialize OpcodeList2 (gain maps) from dt_image_t to a buffer +// Returns a newly allocated buffer with the opcode list, or NULL on error. +// The caller is responsible for freeing the returned buffer. +uint8_t *dt_dng_opcode_serialize_opcode_list_2(const dt_image_t *img, uint32_t *size) +{ + // Only serialize if we have gain maps + if(!img->dng_gain_maps || g_list_length(img->dng_gain_maps) == 0) + { + *size = 0; + return NULL; + } + + // Count total parameter size for all gain maps + uint32_t total_param_size = 0; + GList *list = img->dng_gain_maps; + while(list) + { + dt_dng_gain_map_t *gm = (dt_dng_gain_map_t *)list->data; + // Each gain map has 76 bytes fixed + gain_count * 4 bytes + const uint32_t gain_count = gm->map_points_v * gm->map_points_h * gm->map_planes; + total_param_size += 76 + gain_count * sizeof(float); + list = g_list_next(list); + } + + const int opcode_count = g_list_length(img->dng_gain_maps); + uint32_t total_size = 4 + opcode_count * 16 + total_param_size; + + uint8_t *buf = g_malloc(total_size); + if(!buf) + { + *size = 0; + return NULL; + } + + uint8_t *ptr = buf; + + // Write opcode count + _put_long(ptr, opcode_count); + ptr += 4; + + // Write each gain map opcode + list = img->dng_gain_maps; + while(list) + { + dt_dng_gain_map_t *gm = (dt_dng_gain_map_t *)list->data; + + // Opcode header + _put_long(ptr, OPCODE_ID_GAINMAP); // opcode_id + _put_long(ptr + 4, 0); // flags (0 = mandatory) + _put_long(ptr + 8, 1); // version + + const uint32_t gain_count = gm->map_points_v * gm->map_points_h * gm->map_planes; + const uint32_t param_size = 76 + gain_count * sizeof(float); + _put_long(ptr + 12, param_size); // param_size + + ptr += 16; + + // Write parameters + _put_long(ptr, gm->top); + _put_long(ptr + 4, gm->left); + _put_long(ptr + 8, gm->bottom); + _put_long(ptr + 12, gm->right); + _put_long(ptr + 16, gm->plane); + _put_long(ptr + 20, gm->planes); + _put_long(ptr + 24, gm->row_pitch); + _put_long(ptr + 28, gm->col_pitch); + _put_long(ptr + 32, gm->map_points_v); + _put_long(ptr + 36, gm->map_points_h); + _put_double(ptr + 40, gm->map_spacing_v); + _put_double(ptr + 48, gm->map_spacing_h); + _put_double(ptr + 56, gm->map_origin_v); + _put_double(ptr + 64, gm->map_origin_h); + _put_long(ptr + 72, gm->map_planes); + ptr += 76; + + // Write gain values + for(uint32_t i = 0; i < gain_count; i++) + { + _put_float(ptr, gm->map_gain[i]); + ptr += 4; + } + + list = g_list_next(list); + } + + *size = total_size; + return buf; +} + + +// Serialize OpcodeList3 (lens corrections) from dt_image_t to a buffer +// Returns a newly allocated buffer with the opcode list, or NULL on error. +// The caller is responsible for freeing the returned buffer. +uint8_t *dt_dng_opcode_serialize_opcode_list_3(const dt_image_t *img, uint32_t *size) +{ + const dt_image_correction_data_t *cd = &img->exif_correction_data; + + // Only serialize if we have DNG corrections and at least one type is present + if(img->exif_correction_type != CORRECTION_TYPE_DNG) return NULL; + if(!cd->dng.has_warp && !cd->dng.has_vignette) return NULL; + + // Count how many opcodes we need to serialize + int opcode_count = 0; + if(cd->dng.has_warp) opcode_count++; + if(cd->dng.has_vignette) opcode_count++; + + // Calculate total buffer size: + // - 4 bytes for opcode count + // - For each opcode: 16 bytes header + parameter size + // - Warp rectilinear: 4 (planes) + 8*6 (coefficients per plane) + 8*2 (center) bytes + // - Vignette radial: 8*5 (coefficients) + 8*2 (center) = 56 bytes param + uint32_t total_size = 4; // count at start + if(cd->dng.has_warp) + total_size += 16 + 4 + 8 * 6 * cd->dng.planes + 8 * 2; // header + param + if(cd->dng.has_vignette) + total_size += 16 + 8 * 5 + 8 * 2; // header + param + + uint8_t *buf = g_malloc(total_size); + if(!buf) return NULL; + + uint8_t *ptr = buf; + + // Write opcode count + _put_long(ptr, opcode_count); + ptr += 4; + + // Write warp opcode if present + if(cd->dng.has_warp) + { + // Opcode header: opcode_id (4), flags (4), version (4), param_size (4) + _put_long(ptr, OPCODE_ID_WARP_RECTILINEAR); // opcode_id + _put_long(ptr + 4, 0); // flags (0 = mandatory) + _put_long(ptr + 8, 1); // version + + const uint32_t param_size = 4 + 8 * 6 * cd->dng.planes + 8 * 2; + _put_long(ptr + 12, param_size); // param_size + + ptr += 16; + + // Write parameters: planes (4), then coefficients + _put_long(ptr, cd->dng.planes); + ptr += 4; + + // Write warp coefficients for each plane + for(int p = 0; p < cd->dng.planes; p++) + { + for(int i = 0; i < 6; i++) + { + _put_double(ptr, cd->dng.cwarp[p][i]); + ptr += 8; + } + } + + // Write center coordinates + for(int i = 0; i < 2; i++) + { + _put_double(ptr, cd->dng.centre_warp[i]); + ptr += 8; + } + } + + // Write vignette opcode if present + if(cd->dng.has_vignette) + { + // Opcode header + _put_long(ptr, OPCODE_ID_VIGNETTE_RADIAL); // opcode_id + _put_long(ptr + 4, 0); // flags (0 = mandatory) + _put_long(ptr + 8, 1); // version + + const uint32_t param_size = 8 * (5 + 2); // 5 coefficients + 2 center coordinates + _put_long(ptr + 12, param_size); // param_size + + ptr += 16; + + // Write vignette coefficients + for(int i = 0; i < 5; i++) + { + _put_double(ptr, cd->dng.cvig[i]); + ptr += 8; + } + + // Write center coordinates + for(int i = 0; i < 2; i++) + { + _put_double(ptr, cd->dng.centre_vig[i]); + ptr += 8; + } + } + + *size = total_size; + return buf; +} diff --git a/src/common/dng_opcode.h b/src/common/dng_opcode.h index a57d2c853359..096380090d86 100644 --- a/src/common/dng_opcode.h +++ b/src/common/dng_opcode.h @@ -46,6 +46,10 @@ typedef struct dt_dng_gain_map_t void dt_dng_opcode_process_opcode_list_2(uint8_t *buf, uint32_t size, dt_image_t *img); void dt_dng_opcode_process_opcode_list_3(uint8_t *buf, uint32_t size, dt_image_t *img); +// Serialize DNG opcode lists for writing +uint8_t *dt_dng_opcode_serialize_opcode_list_2(const dt_image_t *img, uint32_t *size); +uint8_t *dt_dng_opcode_serialize_opcode_list_3(const dt_image_t *img, uint32_t *size); + G_END_DECLS // clang-format off diff --git a/src/imageio/imageio_dng.c b/src/imageio/imageio_dng.c index cae9de96c016..b9a7a1b1093c 100644 --- a/src/imageio/imageio_dng.c +++ b/src/imageio/imageio_dng.c @@ -20,6 +20,7 @@ #include "imageio/imageio_dng.h" #include "imageio/imageio_jpeg.h" #include "common/colorspaces_inline_conversions.h" +#include "common/dng_opcode.h" #include "common/exif.h" #include "common/image.h" #include "develop/imageop_math.h" @@ -42,10 +43,43 @@ #define TIFFTAG_PREVIEWCOLORSPACE 50970 #endif +// fallback for older libtiff without DNG opcode tags +#ifndef TIFFTAG_OPCODELIST1 +#define TIFFTAG_OPCODELIST1 51008 +#endif +#ifndef TIFFTAG_OPCODELIST2 +#define TIFFTAG_OPCODELIST2 51009 +#endif +#ifndef TIFFTAG_OPCODELIST3 +#define TIFFTAG_OPCODELIST3 51022 +#endif + // DNG uses SRATIONAL / RATIONAL for matrix and WB tags. libtiff accepts // these as float/double arrays and handles the conversion; we just pass // the values as double +// Write DNG opcode lists to the current TIFF directory +static void _write_dng_opcode_lists(TIFF *tif, const dt_image_t *img) +{ + // Serialize and write OpcodeList2 (gain maps) if present + uint32_t opcode_list_2_size = 0; + uint8_t *opcode_list_2 = dt_dng_opcode_serialize_opcode_list_2(img, &opcode_list_2_size); + if(opcode_list_2 && opcode_list_2_size > 0) + { + TIFFSetField(tif, TIFFTAG_OPCODELIST2, opcode_list_2_size, opcode_list_2); + g_free(opcode_list_2); + } + + // Serialize and write OpcodeList3 (lens corrections) if present + uint32_t opcode_list_3_size = 0; + uint8_t *opcode_list_3 = dt_dng_opcode_serialize_opcode_list_3(img, &opcode_list_3_size); + if(opcode_list_3 && opcode_list_3_size > 0) + { + TIFFSetField(tif, TIFFTAG_OPCODELIST3, opcode_list_3_size, opcode_list_3); + g_free(opcode_list_3); + } +} + // libtiff error/warning handlers — replacing whatever else in the // process may have installed (e.g. ImageMagick's, which crashes on // NULL exception context). these just log to stderr and never abort @@ -474,6 +508,9 @@ int dt_imageio_dng_write_cfa_bayer(const char *filename, TIFFSetField(tif, TIFFTAG_DEFAULTCROPORIGIN, default_crop_origin); TIFFSetField(tif, TIFFTAG_DEFAULTCROPSIZE, default_crop_size); + // Write DNG opcode lists (lens corrections, gain maps) if present + _write_dng_opcode_lists(tif, img); + // scanline write int res = 0; for(int y = 0; y < height && res == 0; y++) @@ -613,6 +650,9 @@ int dt_imageio_dng_write_linear(const char *filename, TIFFSetField(tif, TIFFTAG_DEFAULTCROPORIGIN, default_crop_origin); TIFFSetField(tif, TIFFTAG_DEFAULTCROPSIZE, default_crop_size); + // Write DNG opcode lists (lens corrections, gain maps) if present + _write_dng_opcode_lists(tif, img); + // scanline write: float [0, 1] normalized camRGB -> uint16 // [0, 65535]. BlackLevel=0 and WhiteLevel=65535 let the // re-importer recover the [0, 1] range via the standard raw diff --git a/src/imageio/imageio_dng.h b/src/imageio/imageio_dng.h index 1ea2ef3e97cf..7a2aa02eadb1 100644 --- a/src/imageio/imageio_dng.h +++ b/src/imageio/imageio_dng.h @@ -89,6 +89,7 @@ void dt_imageio_dng_write_float(const char *filename, // - AsShotNeutral from img->wb_coeffs (inverted) // - ColorMatrix1 from img->adobe_XYZ_to_CAM // - Make / Model / UniqueModel from img->camera_maker / camera_model +// - OpcodeList2/3 from img->exif_correction_data (if present) // // @param filename output path (UTF-8) // @param cfa Bayer mosaic (uint16, width * height samples, row-major)