From 7d7562e849735a1d5837f31b2ff059aba2d3ecbc Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 21 Aug 2025 15:17:59 +0200 Subject: [PATCH] Color Management: Support for saving wide gamut images * Bundled ICC profiles for display spaces supported by Blender, and embed them in the image file when saving. * Verified to work for PNG, TIFF, JPEG and WebP, but not all file formats support this. * No ICC profile is written for sRGB currently. It would be a matter of adding an icc file, however this may be a breaking change for some use cases. * Fix save as render of EXR files not properly changing the image colorspace to match. Uses CC0 licensed ICC files from the Compact ICC Profiles project. This does not include support for saving HDR images. While there exist ICC profiles for PQ, they are not well supported and the preferred method for HDR is to write CICP tags. However OpenImageIO support for this is still under development. Ref #144911 Pull Request: https://projects.blender.org/blender/blender/pulls/144565 --- .../datafiles/colormanagement/icc/README.md | 6 ++ .../icc/g24_rec2020_display.icc | 3 + .../icc/g24_rec709_display.icc | 3 + .../icc/g26_xyzd65_display.icc | 3 + .../icc/srgb_p3d65_display.icc | 3 + .../blender/blenkernel/intern/image_save.cc | 19 ++++++ .../editors/space_image/image_buttons.cc | 1 + source/blender/imbuf/IMB_colormanagement.hh | 3 + .../blender/imbuf/intern/colormanagement.cc | 53 +++++++++++++++++ source/blender/imbuf/intern/format_jpeg.cc | 13 ++++ source/blender/imbuf/intern/format_webp.cc | 59 +++++++++++++++---- .../imbuf/intern/oiio/openimageio_support.cc | 13 ++++ 12 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 release/datafiles/colormanagement/icc/README.md create mode 100644 release/datafiles/colormanagement/icc/g24_rec2020_display.icc create mode 100644 release/datafiles/colormanagement/icc/g24_rec709_display.icc create mode 100644 release/datafiles/colormanagement/icc/g26_xyzd65_display.icc create mode 100644 release/datafiles/colormanagement/icc/srgb_p3d65_display.icc diff --git a/release/datafiles/colormanagement/icc/README.md b/release/datafiles/colormanagement/icc/README.md new file mode 100644 index 00000000000..aa83b88715f --- /dev/null +++ b/release/datafiles/colormanagement/icc/README.md @@ -0,0 +1,6 @@ +ICC profiles to embed when writing images. + +The names match the ASWF Color Interop Forum Recommendation. + +From the Compact ICC Profiles project, with CC0-1.0 license. +https://github.com/saucecontrol/Compact-ICC-Profiles diff --git a/release/datafiles/colormanagement/icc/g24_rec2020_display.icc b/release/datafiles/colormanagement/icc/g24_rec2020_display.icc new file mode 100644 index 00000000000..82fd93ecb79 --- /dev/null +++ b/release/datafiles/colormanagement/icc/g24_rec2020_display.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b03c9218e82ab12fc4fcac7f5a0fb10ff53ebc7ad8387ae68a6d8cf5d4dfbaa3 +size 464 diff --git a/release/datafiles/colormanagement/icc/g24_rec709_display.icc b/release/datafiles/colormanagement/icc/g24_rec709_display.icc new file mode 100644 index 00000000000..0c7819abae0 --- /dev/null +++ b/release/datafiles/colormanagement/icc/g24_rec709_display.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d10744483b3448daceacc880566b004cd90aa0ca535f651c8c88ba61646c965 +size 596 diff --git a/release/datafiles/colormanagement/icc/g26_xyzd65_display.icc b/release/datafiles/colormanagement/icc/g26_xyzd65_display.icc new file mode 100644 index 00000000000..40b1b2ac6bf --- /dev/null +++ b/release/datafiles/colormanagement/icc/g26_xyzd65_display.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58ac463ea778e9b70692971dd4ba9071363a03aba77043894162b1ad6c051da7 +size 464 diff --git a/release/datafiles/colormanagement/icc/srgb_p3d65_display.icc b/release/datafiles/colormanagement/icc/srgb_p3d65_display.icc new file mode 100644 index 00000000000..84946657cff --- /dev/null +++ b/release/datafiles/colormanagement/icc/srgb_p3d65_display.icc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb51de38e482ee974c0c76b9689e16aad04bad16e226fed2f30c842d15ff3a3d +size 480 diff --git a/source/blender/blenkernel/intern/image_save.cc b/source/blender/blenkernel/intern/image_save.cc index 03864e70941..5e8ca3dbb52 100644 --- a/source/blender/blenkernel/intern/image_save.cc +++ b/source/blender/blenkernel/intern/image_save.cc @@ -13,6 +13,7 @@ #include "BLI_index_range.hh" #include "BLI_listbase.h" #include "BLI_path_utils.hh" +#include "BLI_string_ref.hh" #include "BLI_string_utf8.h" #include "BLI_task.hh" #include "BLI_vector.hh" @@ -282,6 +283,24 @@ static void image_save_post(ReportList *reports, *r_colorspace_changed = true; } } + else if (opts->save_as_render) { + /* Set the display colorspace that we converted to. */ + const ColorSpace *colorspace = IMB_colormangement_display_get_color_space( + &opts->im_format.display_settings); + if (colorspace) { + blender::StringRefNull colorspace_name = IMB_colormanagement_colorspace_get_name(colorspace); + if (colorspace_name != ima->colorspace_settings.name) { + STRNCPY(ima->colorspace_settings.name, colorspace_name.c_str()); + *r_colorspace_changed = true; + } + } + + /* View transform is now baked in, so don't apply it a second time for viewing. */ + if (ima->flag & IMA_VIEW_AS_RENDER) { + ima->flag &= ~IMA_VIEW_AS_RENDER; + *r_colorspace_changed = true; + } + } } static void imbuf_save_post(ImBuf *ibuf, ImBuf *colormanaged_ibuf) diff --git a/source/blender/editors/space_image/image_buttons.cc b/source/blender/editors/space_image/image_buttons.cc index 5f7a236de7e..ab26d7885a1 100644 --- a/source/blender/editors/space_image/image_buttons.cc +++ b/source/blender/editors/space_image/image_buttons.cc @@ -1066,6 +1066,7 @@ void uiTemplateImageSettings(uiLayout *layout, /* Override color management */ if (color_management) { + col->separator_spacer(); if (uiLayout *panel = col->panel(C, panel_idname ? panel_idname : "settings_color_management", diff --git a/source/blender/imbuf/IMB_colormanagement.hh b/source/blender/imbuf/IMB_colormanagement.hh index 8de4739ef0f..77b733aea95 100644 --- a/source/blender/imbuf/IMB_colormanagement.hh +++ b/source/blender/imbuf/IMB_colormanagement.hh @@ -9,6 +9,7 @@ */ #include "BLI_compiler_compat.h" +#include "BLI_vector.hh" #include "BLI_math_matrix_types.hh" @@ -81,6 +82,8 @@ bool IMB_colormanagement_space_name_is_data(const char *name); bool IMB_colormanagement_space_name_is_scene_linear(const char *name); bool IMB_colormanagement_space_name_is_srgb(const char *name); +blender::Vector IMB_colormanagement_space_icc_profile(const ColorSpace *colorspace); + BLI_INLINE void IMB_colormanagement_get_luminance_coefficients(float r_rgb[3]); /** diff --git a/source/blender/imbuf/intern/colormanagement.cc b/source/blender/imbuf/intern/colormanagement.cc index 334f328fd8d..1756d4e8a09 100644 --- a/source/blender/imbuf/intern/colormanagement.cc +++ b/source/blender/imbuf/intern/colormanagement.cc @@ -29,6 +29,7 @@ #include "MEM_guardedalloc.h" +#include "BLI_fileops.hh" #include "BLI_listbase.h" #include "BLI_math_color.h" #include "BLI_math_color.hh" @@ -1290,6 +1291,58 @@ const char *IMB_colormanagement_srgb_colorspace_name_get() return global_role_default_byte; } +blender::Vector IMB_colormanagement_space_icc_profile(const ColorSpace *colorspace) +{ + /* ICC profiles shipped with Blender are named after the OpenColorIO interop ID. */ + blender::Vector icc_profile; + + const StringRefNull interop_id = colorspace->interop_id(); + if (interop_id.is_empty()) { + return icc_profile; + } + + const std::optional dir = BKE_appdir_folder_id(BLENDER_DATAFILES, + "colormanagement"); + if (!dir.has_value()) { + return icc_profile; + } + + char icc_filename[FILE_MAX]; + STRNCPY(icc_filename, (interop_id + ".icc").c_str()); + BLI_path_make_safe_filename(icc_filename); + + char icc_filepath[FILE_MAX]; + BLI_path_join(icc_filepath, sizeof(icc_filepath), dir->c_str(), "icc", icc_filename); + + blender::fstream f(icc_filepath, std::ios::binary | std::ios::in | std::ios::ate); + if (!f.is_open()) { + /* If we can't find a scene referred filename, try display referred. */ + blender::StringRef icc_filepath_ref = icc_filepath; + if (icc_filepath_ref.endswith("_scene.icc")) { + std::string icc_filepath_display = icc_filepath_ref.drop_suffix(strlen("_scene.icc")) + + "_display.icc"; + f.open(icc_filepath_display, std::ios::binary | std::ios::in | std::ios::ate); + } + + if (!f.is_open()) { + return icc_profile; + } + } + + std::streamsize size = f.tellg(); + if (size <= 0) { + return icc_profile; + } + icc_profile.resize(size); + + f.seekg(0, std::ios::beg); + if (!f.read(icc_profile.data(), icc_profile.size())) { + icc_profile.clear(); + } + + return icc_profile; +} + blender::float3x3 IMB_colormanagement_get_xyz_to_scene_linear() { return blender::float3x3(imbuf_xyz_to_scene_linear); diff --git a/source/blender/imbuf/intern/format_jpeg.cc b/source/blender/imbuf/intern/format_jpeg.cc index f5c04a8fe2e..25d2943aed8 100644 --- a/source/blender/imbuf/intern/format_jpeg.cc +++ b/source/blender/imbuf/intern/format_jpeg.cc @@ -621,6 +621,19 @@ static void write_jpeg(jpeg_compress_struct *cinfo, ImBuf *ibuf) } } + /* Write ICC profile if there is one associated with the colorspace. */ + const ColorSpace *colorspace = ibuf->byte_buffer.colorspace; + if (colorspace) { + blender::Vector icc_profile = IMB_colormanagement_space_icc_profile(colorspace); + if (!icc_profile.is_empty()) { + icc_profile.prepend({'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', 0, 0, 1}); + jpeg_write_marker(cinfo, + JPEG_APP0 + 2, + reinterpret_cast(icc_profile.data()), + icc_profile.size()); + } + } + row_pointer[0] = MEM_malloc_arrayN>( size_t(cinfo->input_components) * size_t(cinfo->image_width), "jpeg row_pointer"); diff --git a/source/blender/imbuf/intern/format_webp.cc b/source/blender/imbuf/intern/format_webp.cc index f53d676fb5c..33ba64d8224 100644 --- a/source/blender/imbuf/intern/format_webp.cc +++ b/source/blender/imbuf/intern/format_webp.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include "BLI_fileops.h" #include "BLI_mmap.h" @@ -212,17 +213,53 @@ bool imb_savewebp(ImBuf *ibuf, const char *filepath, int /*flags*/) return false; } - if (encoded_data != nullptr) { - FILE *fp = BLI_fopen(filepath, "wb"); - if (!fp) { - free(encoded_data); - CLOG_ERROR(&LOG, "Cannot open file for writing: '%s'", filepath); - return false; - } - fwrite(encoded_data, encoded_data_size, 1, fp); - free(encoded_data); - fclose(fp); + if (encoded_data == nullptr) { + return false; } - return true; + WebPMux *mux = WebPMuxNew(); + WebPData image_data = {encoded_data, encoded_data_size}; + WebPMuxSetImage(mux, &image_data, false /* Don't copy data */); + + /* Write ICC profile if there is one associated with the colorspace. */ + const ColorSpace *colorspace = ibuf->byte_buffer.colorspace; + if (colorspace) { + blender::Vector icc_profile = IMB_colormanagement_space_icc_profile(colorspace); + if (!icc_profile.is_empty()) { + WebPData icc_chunk = {reinterpret_cast(icc_profile.data()), + size_t(icc_profile.size())}; + WebPMuxSetChunk(mux, "ICCP", &icc_chunk, true /* copy data */); + } + } + + /* Assemble image and metadata. */ + WebPData output_data; + if (WebPMuxAssemble(mux, &output_data) != WEBP_MUX_OK) { + CLOG_ERROR(&LOG, "Error in mux assemble writing file: '%s'", filepath); + WebPMuxDelete(mux); + WebPFree(encoded_data); + return false; + } + + /* Write to file. */ + bool ok = true; + FILE *fp = BLI_fopen(filepath, "wb"); + if (fp) { + if (fwrite(output_data.bytes, output_data.size, 1, fp) != 1) { + CLOG_ERROR(&LOG, "Unknown error writing file: '%s'", filepath); + ok = false; + } + + fclose(fp); + } + else { + ok = false; + CLOG_ERROR(&LOG, "Cannot open file for writing: '%s'", filepath); + } + + WebPMuxDelete(mux); + WebPFree(encoded_data); + WebPDataClear(&output_data); + + return ok; } diff --git a/source/blender/imbuf/intern/oiio/openimageio_support.cc b/source/blender/imbuf/intern/oiio/openimageio_support.cc index 0ac455b945a..ae99801240a 100644 --- a/source/blender/imbuf/intern/oiio/openimageio_support.cc +++ b/source/blender/imbuf/intern/oiio/openimageio_support.cc @@ -452,6 +452,19 @@ ImageSpec imb_create_write_spec(const WriteContext &ctx, int file_channels, Type } } + /* Write ICC profile if there is one associated with the colorspace. */ + const ColorSpace *colorspace = (ctx.mem_spec.format == TypeDesc::FLOAT) ? + ctx.ibuf->float_buffer.colorspace : + ctx.ibuf->byte_buffer.colorspace; + if (colorspace) { + Vector icc_profile = IMB_colormanagement_space_icc_profile(colorspace); + if (!icc_profile.is_empty()) { + file_spec.attribute("ICCProfile", + OIIO::TypeDesc(OIIO::TypeDesc::UINT8, icc_profile.size()), + icc_profile.data()); + } + } + return file_spec; }