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
This commit is contained in:
Brecht Van Lommel
2025-08-21 15:17:59 +02:00
parent d55ec77e0f
commit 7d7562e849
12 changed files with 168 additions and 11 deletions

View File

@@ -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

Binary file not shown.

BIN
release/datafiles/colormanagement/icc/g24_rec709_display.icc (Stored with Git LFS) Normal file

Binary file not shown.

BIN
release/datafiles/colormanagement/icc/g26_xyzd65_display.icc (Stored with Git LFS) Normal file

Binary file not shown.

BIN
release/datafiles/colormanagement/icc/srgb_p3d65_display.icc (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -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)

View File

@@ -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",

View File

@@ -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<char> IMB_colormanagement_space_icc_profile(const ColorSpace *colorspace);
BLI_INLINE void IMB_colormanagement_get_luminance_coefficients(float r_rgb[3]);
/**

View File

@@ -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<char> IMB_colormanagement_space_icc_profile(const ColorSpace *colorspace)
{
/* ICC profiles shipped with Blender are named after the OpenColorIO interop ID. */
blender::Vector<char> icc_profile;
const StringRefNull interop_id = colorspace->interop_id();
if (interop_id.is_empty()) {
return icc_profile;
}
const std::optional<std::string> 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);

View File

@@ -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<char> 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<const JOCTET *>(icc_profile.data()),
icc_profile.size());
}
}
row_pointer[0] = MEM_malloc_arrayN<std::remove_pointer_t<JSAMPROW>>(
size_t(cinfo->input_components) * size_t(cinfo->image_width), "jpeg row_pointer");

View File

@@ -17,6 +17,7 @@
#include <fcntl.h>
#include <webp/decode.h>
#include <webp/encode.h>
#include <webp/mux.h>
#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<char> icc_profile = IMB_colormanagement_space_icc_profile(colorspace);
if (!icc_profile.is_empty()) {
WebPData icc_chunk = {reinterpret_cast<const uint8_t *>(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;
}

View File

@@ -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<char> 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;
}