diff --git a/DefineOptions.cmake b/DefineOptions.cmake index 15fcad411853..8fd2143ac821 100644 --- a/DefineOptions.cmake +++ b/DefineOptions.cmake @@ -20,6 +20,7 @@ option(USE_JXL "Enable JPEG XL support" ON) option(USE_WEBP "Enable WebP support" ON) option(USE_AVIF "Enable AVIF support" ON) option(USE_HEIF "Enable HEIF/HEIC support" ON) +option(USE_ULTRAHDR "Enable UltraHDR (gain-map JPEG) export support" ON) option(USE_XCF "Enable XCF support" ON) option(USE_ISOBMFF "Enable ISOBMFF support" ON) option(USE_LIBRAW "Enable LibRaw support" ON) diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index e85ea9c18c09..521479d72d8b 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -3043,6 +3043,27 @@ AVIF chroma subsampling + + plugins/imageio/format/ultrahdr/quality + int + 95 + UltraHDR base image JPEG quality + + + + plugins/imageio/format/ultrahdr/gainmap_quality + int + 95 + UltraHDR gain map JPEG quality + + + + plugins/imageio/format/ultrahdr/gainmap_downscale + int + 0 + UltraHDR gain map resolution (0 full, 1 half, 2 quarter) + + plugins/imageio/format/heif/bitdepth diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 40edc6397a3c..53deafcfecaf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -423,6 +423,22 @@ if(USE_HEIF) endif() endif() +# Reset on every configure so a stale value cannot keep the module enabled +# when USE_ULTRAHDR is turned off or libuhdr disappears. +set(UltraHDR_FOUND OFF CACHE INTERNAL "") +if(USE_ULTRAHDR) + # libultrahdr has no CMake CONFIG package, so locate it via pkg-config. + # NOTE: the pkg-config module and link name are "libuhdr" (NOT "libultrahdr"). + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(UltraHDR IMPORTED_TARGET libuhdr) + if(UltraHDR_FOUND) + # Cache so src/imageio/format/CMakeLists.txt can gate the module on it. + set(UltraHDR_FOUND ON CACHE INTERNAL "") + endif() + endif() +endif() + # For now we use the LibRaw submodule if(USE_LIBRAW) if(DONT_USE_INTERNAL_LIBRAW) diff --git a/src/imageio/format/CMakeLists.txt b/src/imageio/format/CMakeLists.txt index 120ff6261dd6..0c57c9935c88 100644 --- a/src/imageio/format/CMakeLists.txt +++ b/src/imageio/format/CMakeLists.txt @@ -50,6 +50,13 @@ if(libavif_FOUND) set_target_properties(avif_format PROPERTIES OUTPUT_NAME avif) endif() +if(UltraHDR_FOUND) + list(APPEND MODULES "ultrahdr_format") + add_library(ultrahdr_format MODULE "ultrahdr.c") + target_link_libraries(ultrahdr_format PRIVATE PkgConfig::UltraHDR) + set_target_properties(ultrahdr_format PROPERTIES OUTPUT_NAME ultrahdr) +endif() + if(TARGET xcf) list(APPEND MODULES "gimp_xcf") add_library(gimp_xcf MODULE "xcf.c") diff --git a/src/imageio/format/ultrahdr.c b/src/imageio/format/ultrahdr.c new file mode 100644 index 000000000000..e7676807947f --- /dev/null +++ b/src/imageio/format/ultrahdr.c @@ -0,0 +1,484 @@ +/* + This file is part of darktable, + Copyright (C) 2019-2025 darktable developers. + + Copyright (c) 2019 Andreas Schneider + + 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 + +#include "bauhaus/bauhaus.h" +#include "common/colorspaces.h" +#include "common/darktable.h" +#include "control/conf.h" +#include "imageio/imageio_common.h" +#include "imageio/imageio_module.h" +#include "imageio/format/imageio_format_api.h" + +#include + +DT_MODULE(1) + +/* + * Gain-map resolution choices. The value is the downscale factor passed to + * uhdr_enc_set_gainmap_scale_factor(): 1 = full resolution, 2 = half, 4 = quarter. + */ +enum ultrahdr_gainmap_downscale_e +{ + ULTRAHDR_GAINMAP_FULL = 0, + ULTRAHDR_GAINMAP_HALF, + ULTRAHDR_GAINMAP_QUARTER, +}; + +static const int ultrahdr_gainmap_scale[] = { 1, 2, 4 }; + +/* Module defaults, also declared in data/darktableconfig.xml.in. Used for the + * slider construction default and the gui_reset() values; the runtime conf + * keys are read with dt_conf_get_int() (the xml supplies the stored default). */ +#define ULTRAHDR_DEFAULT_QUALITY 95 +#define ULTRAHDR_DEFAULT_GAINMAP_QUALITY 95 + +typedef struct dt_imageio_ultrahdr_t +{ + dt_imageio_module_data_t global; + int bpp; + int quality; + int gainmap_quality; + int gainmap_downscale; +} dt_imageio_ultrahdr_t; + +typedef struct dt_imageio_ultrahdr_gui_t +{ + GtkWidget *quality; + GtkWidget *gainmap_quality; + GtkWidget *gainmap_downscale; +} dt_imageio_ultrahdr_gui_t; + +void init(dt_imageio_module_format_t *self) +{ +#ifdef USE_LUA + dt_lua_register_module_member(darktable.lua_state.state, + self, + dt_imageio_ultrahdr_t, + quality, + int); + dt_lua_register_module_member(darktable.lua_state.state, + self, + dt_imageio_ultrahdr_t, + gainmap_quality, + int); + dt_lua_register_module_member(darktable.lua_state.state, + self, + dt_imageio_ultrahdr_t, + gainmap_downscale, + int); +#endif +} + +void cleanup(dt_imageio_module_format_t *self) +{ +} + +int write_image(struct dt_imageio_module_data_t *data, + const char *filename, + const void *in, + dt_colorspaces_color_profile_type_t over_type, + const char *over_filename, + void *exif, + int exif_len, + dt_imgid_t imgid, + int num, + int total, + struct dt_dev_pixelpipe_t *pipe, + const gboolean export_masks) +{ + dt_imageio_ultrahdr_t *d = (dt_imageio_ultrahdr_t *)data; + + uhdr_codec_private_t *enc = NULL; + uint32_t *packed = NULL; + FILE *f = NULL; + int rc = 1; + + const size_t width = d->global.width; + const size_t height = d->global.height; + + /* + * Determine the actual (export vs colorout) color profile used. UltraHDR + * requires a PQ (SMPTE ST 2084) output profile: colorout has already baked + * the PQ curve into the float samples, so the buffer is PQ-encoded, in the + * chosen primaries (Rec2020 or P3). We pass it to libultrahdr as UHDR_CT_PQ. + */ + const dt_colorspaces_color_profile_t *cp = + dt_colorspaces_get_output_profile(imgid, over_type, over_filename); + + uhdr_color_gamut_t cg; + switch(cp->type) + { + case DT_COLORSPACE_PQ_REC2020: + cg = UHDR_CG_BT_2100; + break; + case DT_COLORSPACE_PQ_P3: + cg = UHDR_CG_DISPLAY_P3; + break; + default: + dt_print(DT_DEBUG_IMAGEIO, + "UltraHDR export requires a PQ output color profile " + "(PQ Rec2020 or PQ P3); got %s", + dt_colorspaces_get_name(cp->type, filename)); + return 1; + } + + dt_print(DT_DEBUG_IMAGEIO, + "Exporting UltraHDR image [%s] " + "[width: %zu, height: %zu, quality: %d, gain map quality: %d, " + "gain map scale: 1/%d]", + filename, + width, + height, + d->quality, + d->gainmap_quality, + ultrahdr_gainmap_scale[d->gainmap_downscale]); + + /* + * Pack the interleaved float32 RGBA buffer into RGBA1010102. + * + * The float samples are PQ-encoded and normalized to [0, 1]. We quantize each + * of R, G, B to 10 bits and pack little-endian per the libultrahdr header: + * Red 9:0, Green 19:10, Blue 29:20, Alpha 31:30 (alpha is opaque, 0x3). + */ + packed = dt_alloc_align_type(uint32_t, width * height); + if(packed == NULL) + { + dt_print(DT_DEBUG_IMAGEIO, "Failed to allocate UltraHDR pixel buffer"); + goto out; + } + + const float *const restrict in_data = (const float *)in; + + DT_OMP_FOR_SIMD() + for(size_t k = 0; k < width * height; k++) + { + const float *const restrict in_pixel = &in_data[4 * k]; + + const uint32_t r = (uint32_t)CLAMP(lroundf(CLAMP(in_pixel[0], 0.0f, 1.0f) * 1023.0f), 0, 1023); + const uint32_t g = (uint32_t)CLAMP(lroundf(CLAMP(in_pixel[1], 0.0f, 1.0f) * 1023.0f), 0, 1023); + const uint32_t b = (uint32_t)CLAMP(lroundf(CLAMP(in_pixel[2], 0.0f, 1.0f) * 1023.0f), 0, 1023); + + packed[k] = r | (g << 10) | (b << 20) | (0x3u << 30); + } + + uhdr_raw_image_t img = { 0 }; + img.fmt = UHDR_IMG_FMT_32bppRGBA1010102; + img.cg = cg; + img.ct = UHDR_CT_PQ; + img.range = UHDR_CR_FULL_RANGE; + img.w = (unsigned int)width; + img.h = (unsigned int)height; + img.planes[UHDR_PLANE_PACKED] = packed; + img.stride[UHDR_PLANE_PACKED] = (unsigned int)width; /* stride in pixels */ + + enc = uhdr_create_encoder(); + if(enc == NULL) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_create_encoder failed"); + goto out; + } + + uhdr_error_info_t e; + + /* + * API-0 single-input encode: feed only the HDR rendition. libultrahdr + * internally tone-maps to an SDR base, computes the ISO 21496-1 gain map, + * and muxes the base JPEG + gain map + MPF container. No SDR input needed. + */ + e = uhdr_enc_set_raw_image(enc, &img, UHDR_HDR_IMG); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_enc_set_raw_image failed: %s", e.detail); + goto out; + } + + e = uhdr_enc_set_quality(enc, d->quality, UHDR_BASE_IMG); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_enc_set_quality (base) failed: %s", e.detail); + goto out; + } + + e = uhdr_enc_set_quality(enc, d->gainmap_quality, UHDR_GAIN_MAP_IMG); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_enc_set_quality (gain map) failed: %s", e.detail); + goto out; + } + + e = uhdr_enc_set_gainmap_scale_factor(enc, ultrahdr_gainmap_scale[d->gainmap_downscale]); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_enc_set_gainmap_scale_factor failed: %s", e.detail); + goto out; + } + + e = uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_enc_set_output_format failed: %s", e.detail); + goto out; + } + + e = uhdr_encode(enc); + if(e.error_code != UHDR_CODEC_OK) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_encode failed: %s", e.detail); + goto out; + } + + uhdr_compressed_image_t *out = uhdr_get_encoded_stream(enc); + if(out == NULL || out->data == NULL || out->data_sz == 0) + { + dt_print(DT_DEBUG_IMAGEIO, "uhdr_get_encoded_stream returned empty data"); + goto out; + } + + /* + * Write the encoder-owned buffer to disk before releasing the encoder. + */ + f = g_fopen(filename, "wb"); + if(f == NULL) + { + dt_print(DT_DEBUG_IMAGEIO, "Failed to open `%s' for writing", filename); + goto out; + } + + const size_t cnt = fwrite(out->data, 1, out->data_sz, f); + fclose(f); + f = NULL; + + if(cnt != out->data_sz) + { + dt_print(DT_DEBUG_IMAGEIO, "Failed to write UltraHDR image `%s'", filename); + g_unlink(filename); + goto out; + } + + /* + * TODO: embed EXIF/XMP. libultrahdr offers uhdr_enc_set_exif_data(), but it + * must be set before uhdr_encode() and darktable's exif blob would need + * conversion; left out for v1 (flags() returns 0). No metadata is written. + */ + + rc = 0; /* success */ + +out: + if(f != NULL) + fclose(f); + if(enc != NULL) + uhdr_release_encoder(enc); + dt_free_align(packed); + + return rc; +} + +size_t params_size(dt_imageio_module_format_t *self) +{ + return sizeof(dt_imageio_ultrahdr_t); +} + +void *get_params(dt_imageio_module_format_t *self) +{ + dt_imageio_ultrahdr_t *d = calloc(1, sizeof(dt_imageio_ultrahdr_t)); + + if(d == NULL) + return NULL; + + d->bpp = 32; + d->quality = dt_conf_get_int("plugins/imageio/format/ultrahdr/quality"); + d->gainmap_quality = dt_conf_get_int("plugins/imageio/format/ultrahdr/gainmap_quality"); + d->gainmap_downscale = dt_conf_get_int("plugins/imageio/format/ultrahdr/gainmap_downscale"); + + if(d->gainmap_downscale < 0 || d->gainmap_downscale > ULTRAHDR_GAINMAP_QUARTER) + d->gainmap_downscale = ULTRAHDR_GAINMAP_FULL; + + return d; +} + +int set_params(dt_imageio_module_format_t *self, + const void *params, + const int size) +{ + if(size != self->params_size(self)) + return 1; + const dt_imageio_ultrahdr_t *d = (dt_imageio_ultrahdr_t *)params; + + dt_imageio_ultrahdr_gui_t *g = self->gui_data; + dt_bauhaus_slider_set(g->quality, d->quality); + dt_bauhaus_slider_set(g->gainmap_quality, d->gainmap_quality); + dt_bauhaus_combobox_set(g->gainmap_downscale, d->gainmap_downscale); + + return 0; +} + +void free_params(dt_imageio_module_format_t *self, + dt_imageio_module_data_t *params) +{ + free(params); +} + +int bpp(struct dt_imageio_module_data_t *data) +{ + return 32; /* always request float */ +} + +int levels(struct dt_imageio_module_data_t *data) +{ + /* PQ-encoded float buffer packed to 10-bit RGBA */ + return IMAGEIO_RGB | IMAGEIO_INT10; +} + +const char *mime(dt_imageio_module_data_t *data) +{ + return "image/jpeg"; +} + +const char *extension(dt_imageio_module_data_t *data) +{ + return "jpg"; +} + +const char *name() +{ + return _("UltraHDR JPEG"); +} + +int flags(struct dt_imageio_module_data_t *data) +{ + /* No XMP/EXIF embedding for v1, see TODO in write_image(). */ + return 0; +} + +int dimension(struct dt_imageio_module_format_t *self, + struct dt_imageio_module_data_t *data, + uint32_t *width, + uint32_t *height) +{ + /* No hard limit imposed by the module. */ + return 0; +} + +static void quality_changed(GtkWidget *slider, gpointer user_data) +{ + const int quality = (int)dt_bauhaus_slider_get(slider); + dt_conf_set_int("plugins/imageio/format/ultrahdr/quality", quality); +} + +static void gainmap_quality_changed(GtkWidget *slider, gpointer user_data) +{ + const int quality = (int)dt_bauhaus_slider_get(slider); + dt_conf_set_int("plugins/imageio/format/ultrahdr/gainmap_quality", quality); +} + +static void gainmap_downscale_changed(GtkWidget *widget, gpointer user_data) +{ + const int idx = dt_bauhaus_combobox_get(widget); + dt_conf_set_int("plugins/imageio/format/ultrahdr/gainmap_downscale", idx); +} + +void gui_init(dt_imageio_module_format_t *self) +{ + dt_imageio_ultrahdr_gui_t *gui = malloc(sizeof(dt_imageio_ultrahdr_gui_t)); + self->gui_data = (void *)gui; + + const int quality = dt_conf_get_int("plugins/imageio/format/ultrahdr/quality"); + const int gainmap_quality = dt_conf_get_int("plugins/imageio/format/ultrahdr/gainmap_quality"); + const int gainmap_downscale = dt_conf_get_int("plugins/imageio/format/ultrahdr/gainmap_downscale"); + + /* + * Base image quality slider + */ + gui->quality = dt_bauhaus_slider_new_with_range((dt_iop_module_t *)self, + 0, /* min */ + 100, /* max */ + 1, /* step */ + ULTRAHDR_DEFAULT_QUALITY, /* default */ + 0); /* digits */ + dt_bauhaus_widget_set_label(gui->quality, NULL, N_("quality")); + gtk_widget_set_tooltip_text(gui->quality, + _("the quality of the SDR base image, less quality means fewer details")); + dt_bauhaus_slider_set(gui->quality, quality); + + /* + * Gain map quality slider + */ + gui->gainmap_quality = dt_bauhaus_slider_new_with_range((dt_iop_module_t *)self, + 0, /* min */ + 100, /* max */ + 1, /* step */ + ULTRAHDR_DEFAULT_GAINMAP_QUALITY, /* default */ + 0); /* digits */ + dt_bauhaus_widget_set_label(gui->gainmap_quality, NULL, N_("gain map quality")); + gtk_widget_set_tooltip_text(gui->gainmap_quality, + _("the quality of the HDR gain map, less quality means a coarser gain map")); + dt_bauhaus_slider_set(gui->gainmap_quality, gainmap_quality); + + /* + * Gain map resolution combo box + */ + DT_BAUHAUS_COMBOBOX_NEW_FULL(gui->gainmap_downscale, self, NULL, N_("gain map resolution"), + _("resolution of the HDR gain map relative to the image.\n\n" + "full - same resolution as the image\n" + "half - half resolution (smaller file)\n" + "quarter - quarter resolution (smallest file)"), + gainmap_downscale, gainmap_downscale_changed, self, + N_("full"), N_("half"), N_("quarter")); + + g_signal_connect(G_OBJECT(gui->quality), + "value-changed", + G_CALLBACK(quality_changed), + NULL); + g_signal_connect(G_OBJECT(gui->gainmap_quality), + "value-changed", + G_CALLBACK(gainmap_quality_changed), + NULL); + + self->widget = dt_gui_vbox(gui->quality, gui->gainmap_quality, gui->gainmap_downscale); +} + +void gui_cleanup(dt_imageio_module_format_t *self) +{ + free(self->gui_data); +} + +void gui_reset(dt_imageio_module_format_t *self) +{ + dt_imageio_ultrahdr_gui_t *gui = self->gui_data; + + const int quality = ULTRAHDR_DEFAULT_QUALITY; + const int gainmap_quality = ULTRAHDR_DEFAULT_GAINMAP_QUALITY; + const int gainmap_downscale = ULTRAHDR_GAINMAP_FULL; + + dt_bauhaus_slider_set(gui->quality, quality); + dt_bauhaus_slider_set(gui->gainmap_quality, gainmap_quality); + dt_bauhaus_combobox_set(gui->gainmap_downscale, gainmap_downscale); +} + +// 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