diff --git a/src/doc/imageioapi.rst b/src/doc/imageioapi.rst index a674ffdb31..588b2b0ca7 100644 --- a/src/doc/imageioapi.rst +++ b/src/doc/imageioapi.rst @@ -264,6 +264,14 @@ just exist in the OIIO namespace as general utilities. (See .. doxygenfunction:: OIIO::set_colorspace_rec709_gamma +.. doxygenfunction:: OIIO::is_colorspace_srgb + +.. doxygenfunction:: OIIO::get_colorspace_rec709_gamma + +.. doxygenfunction:: OIIO::get_colorspace_icc_profile + +.. doxygenfunction:: OIIO::get_colorspace_cicp + .. doxygenfunction:: OIIO::equivalent_colorspace | diff --git a/src/doc/pythonbindings.rst b/src/doc/pythonbindings.rst index 981a2d9ec5..2506889000 100644 --- a/src/doc/pythonbindings.rst +++ b/src/doc/pythonbindings.rst @@ -4140,6 +4140,71 @@ details. This function was added in OpenImageIO 3.0. +.. py:method:: is_colorspace_srgb (spec, default_to_srgb = True) + + Returns `True` if for the purpose of interop, the metadata of the `spec` + specifies a color space that should be encoded as sRGB. + + If `default_to_srgb` is `True`, the color space will be assumed to be + sRGB if no color space was specified in the spec. + + Example: + + .. code-block:: python + + if oiio.is_colorspace_srgb (spec) : + print ("The image is sRGB") + + This function was added in OpenImageIO 3.1. + + +.. py:method:: get_colorspace_rec709_gamma (spec) + + If the metadata of the `spec` specifies a color space with Rec709 + primaries and gamma transfer function, return the gamma value. If not, + return zero. + + Example: + + .. code-block:: python + + gamma = oiio.get_colorspace_rec709_gamma (spec) + + This function was added in OpenImageIO 3.1. + + +.. py:method:: get_colorspace_icc_profile (spec, from_colorspace = True) + + Returns the ICC profile from the metadata of the `spec` as a `bytes` + object, either from an "ICCProfile" attribute or from the color space if + `from_colorspace` is `True`. Returns `None` if not found. + + Example: + + .. code-block:: python + + icc_profile = oiio.get_colorspace_icc_profile (spec) + + This function was added in OpenImageIO 3.1. + + +.. py:method:: get_colorspace_cicp (spec, from_colorspace = True) + + Returns the CICP code from the metadata of the `spec` as a list of 4 + ints, either from a "CICP" attribute or from the color space if + `from_colorspace` is `True`. Returns `None` if not found. + + Example: + + .. code-block:: python + + cicp = oiio.get_colorspace_cicp (spec) + if cicp: + primaries, transfer, matrix, range = cicp + + This function was added in OpenImageIO 3.1. + + .. py:method:: equivalent_colorspace (a, b) Return `True` if the color spaces `a` and `b` are equivalent in the diff --git a/src/dpx.imageio/dpxoutput.cpp b/src/dpx.imageio/dpxoutput.cpp index 0779efc29d..5f74bf4b4d 100644 --- a/src/dpx.imageio/dpxoutput.cpp +++ b/src/dpx.imageio/dpxoutput.cpp @@ -427,18 +427,15 @@ DPXOutput::prep_subimage(int s, bool allocate) m_desc = get_image_descriptor(); // transfer function - const ColorConfig& colorconfig = ColorConfig::default_colorconfig(); - std::string colorspace = spec_s.get_string_attribute("oiio:ColorSpace", ""); - if (colorconfig.equivalent(colorspace, "lin_rec709_scene")) - m_transfer = dpx::kLinear; - else if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) + const float gamma = get_colorspace_rec709_gamma(spec_s); + if (is_colorspace_srgb(spec_s, false)) m_transfer = dpx::kITUR709; - else if (colorconfig.equivalent(colorspace, "g22_rec709_scene") - || colorconfig.equivalent(colorspace, "g24_rec709_scene") - || colorconfig.equivalent(colorspace, "g18_rec709_scene") - || Strutil::istarts_with(colorspace, "Gamma")) + else if (gamma == 1.0f) + m_transfer = dpx::kLinear; + else if (gamma != 0.0f) m_transfer = dpx::kUserDefined; - else if (colorconfig.equivalent(colorspace, "KodakLog")) + else if (ColorConfig::default_colorconfig().equivalent( + spec_s.get_string_attribute("oiio:ColorSpace"), "KodakLog")) m_transfer = dpx::kLogarithmic; else { std::string dpxtransfer = spec_s.get_string_attribute("dpx:Transfer", diff --git a/src/heif.imageio/heifoutput.cpp b/src/heif.imageio/heifoutput.cpp index 5371af8f72..0d31d0065a 100644 --- a/src/heif.imageio/heifoutput.cpp +++ b/src/heif.imageio/heifoutput.cpp @@ -275,12 +275,7 @@ HeifOutput::close() std::unique_ptr nclx(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free); - const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - const ParamValue* p = m_spec.find_attribute("CICP", - TypeDesc(TypeDesc::INT, 4)); - string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); - cspan cicp = (p) ? p->as_cspan() - : colorconfig.get_cicp(colorspace); + cspan cicp = get_colorspace_cicp(m_spec); if (!cicp.empty()) { nclx->color_primaries = heif_color_primaries(cicp[0]); nclx->transfer_characteristics = heif_transfer_characteristics( diff --git a/src/include/OpenImageIO/imageio.h b/src/include/OpenImageIO/imageio.h index a20af2284c..d5234312f9 100644 --- a/src/include/OpenImageIO/imageio.h +++ b/src/include/OpenImageIO/imageio.h @@ -4377,6 +4377,39 @@ OIIO_API void set_colorspace(ImageSpec& spec, string_view name); /// @version 3.0 OIIO_API void set_colorspace_rec709_gamma(ImageSpec& spec, float gamma); +/// Returns true if for the purpose of interop, the metadata of the `spec` +/// specifies a color space that should be encoded as sRGB. +/// +/// If `default_to_srgb` is true, the color space will be assumed to be sRGB +/// if no color space was specified in the spec. +/// +/// @version 3.1 +OIIO_API bool is_colorspace_srgb(const ImageSpec& spec, + bool default_to_srgb = true); + +/// If the metadata of the `spec` specifies a color space with Rec709 +/// primaries and gamma transfer function, return the gamma value. If not, +/// return zero. +/// +/// @version 3.1 +OIIO_API float get_colorspace_rec709_gamma(const ImageSpec& spec); + +/// Returns the ICC profile from the metadata of the `spec`, either from an +/// "ICCProfile" attribute or from the color space if `from_colorspace` is +/// true. Returns an empty vector if not found. +/// +/// @version 3.1 +OIIO_API std::vector +get_colorspace_icc_profile(const ImageSpec& spec, bool from_colorspace = true); + +/// Returns the CICP code from the metadata of the `spec`, either from a +/// "CICP" attribute or from the color space if `from_colorspace` is true. +/// Returns a cspan of 4 ints, or an empty span if not found. +/// +/// @version 3.1 +OIIO_API cspan get_colorspace_cicp(const ImageSpec& spec, + bool from_colorspace = true); + /// Are the two named color spaces equivalent, based on the default color /// config in effect? @@ -4764,6 +4797,9 @@ using v3_1::debugfmt; using v3_1::declare_imageio_format; using v3_1::equivalent_colorspace; using v3_1::errorfmt; +using v3_1::get_colorspace_cicp; +using v3_1::get_colorspace_icc_profile; +using v3_1::get_colorspace_rec709_gamma; using v3_1::get_extension_map; using v3_1::get_float_attribute; using v3_1::get_int_attribute; @@ -4771,6 +4807,7 @@ using v3_1::get_string_attribute; using v3_1::getattribute; using v3_1::geterror; using v3_1::has_error; +using v3_1::is_colorspace_srgb; using v3_1::is_imageio_format_name; using v3_1::log_time; using v3_1::openimageio_version; diff --git a/src/iv/imageviewer.cpp b/src/iv/imageviewer.cpp index 44a7de54f2..06ae9db1b0 100644 --- a/src/iv/imageviewer.cpp +++ b/src/iv/imageviewer.cpp @@ -48,18 +48,6 @@ #include "ivutils.h" -namespace { - -inline bool -IsSpecSrgb(const ImageSpec& spec) -{ - return equivalent_colorspace(spec.get_string_attribute("oiio:ColorSpace"), - "srgb_rec709_scene"); -} - -} // namespace - - // clang-format off static const char *s_file_filters = "" "Image Files (*.bmp *.cin *.dcm *.dds *.dpx *.fits *.gif *.hdr *.ico *.iff " @@ -1265,7 +1253,8 @@ ImageViewer::loadCurrentImage(int subimage, int miplevel) //std::cerr << "Loading HALF-FLOAT as FLOAT\n"; read_format = TypeDesc::FLOAT; } - if (IsSpecSrgb(image_spec) && !glwin->is_srgb_capable()) { + if (is_colorspace_srgb(image_spec, false) + && !glwin->is_srgb_capable()) { // If the image is in sRGB, but OpenGL can't load sRGB textures then // we'll need to do the transformation on the CPU after loading the // image. We (so far) can only do this with UINT8 images, so make @@ -1280,7 +1269,8 @@ ImageViewer::loadCurrentImage(int subimage, int miplevel) read_format = TypeDesc::UINT8; allow_transforms = true; - if (IsSpecSrgb(image_spec) && !glwin->is_srgb_capable()) + if (is_colorspace_srgb(image_spec, false) + && !glwin->is_srgb_capable()) srgb_transform = true; } @@ -1466,7 +1456,7 @@ ImageViewer::exposureMinusOneTenthStop() img->exposure(img->exposure() - 0.1); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1485,7 +1475,7 @@ ImageViewer::exposureMinusOneHalfStop() img->exposure(img->exposure() - 0.5); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1504,7 +1494,7 @@ ImageViewer::exposurePlusOneTenthStop() img->exposure(img->exposure() + 0.1); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1523,7 +1513,7 @@ ImageViewer::exposurePlusOneHalfStop() img->exposure(img->exposure() + 0.5); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1543,7 +1533,7 @@ ImageViewer::gammaMinus() img->gamma(img->gamma() - 0.05); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1562,7 +1552,7 @@ ImageViewer::gammaPlus() img->gamma(img->gamma() + 0.05); if (!glwin->is_glsl_capable()) { bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)current_color_mode(), current_channel()); displayCurrentImage(); @@ -1594,8 +1584,9 @@ ImageViewer::viewChannel(int c, COLOR_MODE colormode) if (!glwin->is_glsl_capable()) { IvImage* img = cur(); if (img) { - bool srgb_transform = (!glwin->is_srgb_capable() - && IsSpecSrgb(img->spec())); + bool srgb_transform + = (!glwin->is_srgb_capable() + && is_colorspace_srgb(img->spec(), false)); img->pixel_transform(srgb_transform, (int)colormode, c); } } else { @@ -2556,4 +2547,4 @@ ImageViewer::flipVertical() spec->attribute("Orientation", curr_orientation); } displayCurrentImage(); -} \ No newline at end of file +} diff --git a/src/iv/ivgl.cpp b/src/iv/ivgl.cpp index 2d773d674c..e96e523514 100644 --- a/src/iv/ivgl.cpp +++ b/src/iv/ivgl.cpp @@ -2198,9 +2198,7 @@ IvGL::typespec_to_opengl(const ImageSpec& spec, int nchannels, GLenum& gltype, break; } - bool issrgb - = equivalent_colorspace(spec.get_string_attribute("oiio:ColorSpace"), - "srgb_rec709_scene"); + bool issrgb = is_colorspace_srgb(spec, false); glinternalformat = nchannels; if (nchannels == 1) { diff --git a/src/jpeg.imageio/jpegoutput.cpp b/src/jpeg.imageio/jpegoutput.cpp index b5ff4195bc..94b2388431 100644 --- a/src/jpeg.imageio/jpegoutput.cpp +++ b/src/jpeg.imageio/jpegoutput.cpp @@ -312,8 +312,7 @@ JpgOutput::open(const std::string& name, const ImageSpec& newspec, } } - if (equivalent_colorspace(m_spec.get_string_attribute("oiio:ColorSpace"), - "srgb_rec709_scene")) + if (is_colorspace_srgb(m_spec, false)) m_spec.attribute("Exif:ColorSpace", 1); // Write EXIF info @@ -364,37 +363,32 @@ JpgOutput::open(const std::string& name, const ImageSpec& newspec, m_spec.set_format(TypeDesc::UINT8); // JPG is only 8 bit // Write ICC profile, if we have anything - if (auto icc_profile_parameter = m_spec.find_attribute(ICC_PROFILE_ATTR)) { - cspan icc_profile((unsigned char*) - icc_profile_parameter->data(), - icc_profile_parameter->type().size()); - if (icc_profile.size() && icc_profile.data()) { - /* Calculate the number of markers we'll need, rounding up of course */ - size_t num_markers = icc_profile.size() / MAX_DATA_BYTES_IN_MARKER; - if (num_markers * MAX_DATA_BYTES_IN_MARKER - != std::size(icc_profile)) - num_markers++; - int curr_marker = 1; /* per spec, count starts at 1*/ - std::vector profile(MAX_DATA_BYTES_IN_MARKER - + ICC_HEADER_SIZE); - size_t icc_profile_length = icc_profile.size(); - while (icc_profile_length > 0) { - // length of profile to put in this marker - size_t length = std::min(icc_profile_length, - size_t(MAX_DATA_BYTES_IN_MARKER)); - icc_profile_length -= length; - // Write the JPEG marker header (APP2 code and marker length) - strcpy((char*)profile.data(), "ICC_PROFILE"); // NOSONAR - profile[11] = 0; - profile[12] = curr_marker; - profile[13] = (JOCTET)num_markers; - OIIO_ASSERT(profile.size() >= ICC_HEADER_SIZE + length); - spancpy(make_span(profile), ICC_HEADER_SIZE, icc_profile, - length * (curr_marker - 1), length); - jpeg_write_marker(&m_cinfo, JPEG_APP0 + 2, profile.data(), - ICC_HEADER_SIZE + length); - curr_marker++; - } + std::vector icc_profile = get_colorspace_icc_profile(m_spec); + if (icc_profile.size()) { + /* Calculate the number of markers we'll need, rounding up of course */ + size_t num_markers = icc_profile.size() / MAX_DATA_BYTES_IN_MARKER; + if (num_markers * MAX_DATA_BYTES_IN_MARKER != std::size(icc_profile)) + num_markers++; + int curr_marker = 1; /* per spec, count starts at 1*/ + std::vector profile(MAX_DATA_BYTES_IN_MARKER + ICC_HEADER_SIZE); + size_t icc_profile_length = icc_profile.size(); + while (icc_profile_length > 0) { + // length of profile to put in this marker + size_t length = std::min(icc_profile_length, + size_t(MAX_DATA_BYTES_IN_MARKER)); + icc_profile_length -= length; + // Write the JPEG marker header (APP2 code and marker length) + strcpy((char*)profile.data(), "ICC_PROFILE"); // NOSONAR + profile[11] = 0; + profile[12] = curr_marker; + profile[13] = (JOCTET)num_markers; + OIIO_ASSERT(profile.size() >= ICC_HEADER_SIZE + length); + spancpy(make_span(profile), ICC_HEADER_SIZE, + cspan(icc_profile), length * (curr_marker - 1), + length); + jpeg_write_marker(&m_cinfo, JPEG_APP0 + 2, profile.data(), + ICC_HEADER_SIZE + length); + curr_marker++; } } diff --git a/src/jpeg2000.imageio/jpeg2000output.cpp b/src/jpeg2000.imageio/jpeg2000output.cpp index a34f0f5ae5..c8650904f6 100644 --- a/src/jpeg2000.imageio/jpeg2000output.cpp +++ b/src/jpeg2000.imageio/jpeg2000output.cpp @@ -66,6 +66,9 @@ class Jpeg2000Output final : public ImageOutput { bool m_convert_alpha; //< Do we deassociate alpha? std::vector m_tilebuffer; std::vector m_scratch; +#if 0 + std::vector m_icc_profile; +#endif #ifdef USE_OPENJPH @@ -81,6 +84,9 @@ class Jpeg2000Output final : public ImageOutput { m_codec = NULL; m_stream = NULL; m_convert_alpha = true; +#if 0 + m_icc_profile.clear(); +#endif ioproxy_clear(); } @@ -481,10 +487,13 @@ Jpeg2000Output::create_jpeg2000_image() // someboody comes along that desperately needs JPEG2000 and ICC // profiles, maybe they will be motivated enough to track down the // problem. - const ParamValue *icc = m_spec.find_attribute ("ICCProfile"); - if (icc && icc->type().basetype == TypeDesc::UINT8 && icc->type().arraylen > 0) { - m_image->icc_profile_len = icc->type().arraylen; - m_image->icc_profile_buf = (unsigned char *) icc->data(); + // NOTE: openjpeg does not copy the profile and m_image outlives this + // function, so the bytes are kept in a member rather than a local. + m_icc_profile = get_colorspace_icc_profile(m_spec); + if (m_icc_profile.size()) { + m_image->icc_profile_len + = decltype(m_image->icc_profile_len)(m_icc_profile.size()); + m_image->icc_profile_buf = (unsigned char*)m_icc_profile.data(); } #endif diff --git a/src/jpegxl.imageio/jxloutput.cpp b/src/jpegxl.imageio/jxloutput.cpp index 37fff33817..9f444f82ed 100644 --- a/src/jpegxl.imageio/jxloutput.cpp +++ b/src/jpegxl.imageio/jxloutput.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -542,30 +541,19 @@ JxlOutput::save_image(const void* data) bool wrote_colorspace = false; // Write the ICC profile, if available - const ParamValue* icc_profile_parameter = m_spec.find_attribute( - "ICCProfile"); - if (icc_profile_parameter != nullptr) { - unsigned char* icc_profile - = (unsigned char*)icc_profile_parameter->data(); - uint32_t length = icc_profile_parameter->type().size(); - if (icc_profile && length) { - if (JXL_ENC_SUCCESS - != JxlEncoderSetICCProfile(m_encoder.get(), icc_profile, - length)) { - errorfmt("JxlEncoderSetICCProfile failed\n"); - } + std::vector icc_profile = get_colorspace_icc_profile(m_spec); + if (icc_profile.size()) { + if (JXL_ENC_SUCCESS + != JxlEncoderSetICCProfile(m_encoder.get(), icc_profile.data(), + icc_profile.size())) { + errorfmt("JxlEncoderSetICCProfile failed\n"); + } else { wrote_colorspace = true; } } // Write CICP - const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - const ParamValue* p = m_spec.find_attribute("CICP", - TypeDesc(TypeDesc::INT, 4)); - string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); - cspan cicp = (p) ? p->as_cspan() - : (!wrote_colorspace) ? colorconfig.get_cicp(colorspace) - : cspan(); + cspan cicp = get_colorspace_cicp(m_spec, !wrote_colorspace); if (!cicp.empty()) { // JXL only has a subset of CICP, only write if supported. Custom // primaries and white point are not currently used but could help diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 7a56da9864..4b46695ffe 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -3094,5 +3094,89 @@ set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) ColorConfig::default_colorconfig().set_colorspace_rec709_gamma(spec, gamma); } +// Parse a color space name of the form "g_rec709_(scene|display)". +static float +rec709_colorspace_gamma(string_view colorspace) +{ + if (!Strutil::parse_prefix(colorspace, "g")) + return 0.0f; + int g10 = 0; + if (!Strutil::parse_int(colorspace, g10) || g10 <= 0) + return 0.0f; + if (colorspace != "_rec709_scene" && colorspace != "_rec709_display") + return 0.0f; + return float(g10) / 10.0f; +} + +float +get_colorspace_rec709_gamma(const ImageSpec& spec) +{ + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + string_view colorspace = spec.get_string_attribute("oiio:ColorSpace"); + string_view interop_id = colorconfig.get_color_interop_id(colorspace); + + // Gamma interop IDs, as well as arbitrary names that do not have an + // official interop ID as generated by set_colorspace_rec709_gamma(). + const float gamma = rec709_colorspace_gamma( + interop_id.empty() ? colorspace : interop_id); + + // Backwards compatibility, scene_linear is not necessarily Rec.709 + if (colorconfig.equivalent(colorspace, "linear") + || colorconfig.equivalent(colorspace, "scene_linear") + || interop_id == "lin_rec709_scene") + return 1.0f; + else if (gamma != 0.0f) + return gamma; + // Backwards compatibility, this is DEPRECATED(3.1) + else if (Strutil::istarts_with(colorspace, "Gamma")) { + Strutil::parse_word(colorspace); + float g = Strutil::from_string(colorspace); + if (g >= 0.01f && g <= 10.0f /* sanity check */) + return g; + } + + // Obsolete "oiio:Gamma" attribute for backwards compatibility + return spec.get_float_attribute("oiio:Gamma", 0.0f); +} + +bool +is_colorspace_srgb(const ImageSpec& spec, bool default_to_srgb) +{ + string_view colorspace = spec.get_string_attribute("oiio:ColorSpace"); + if (default_to_srgb && colorspace.empty()) { + return true; + } + + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + string_view interop_id = colorconfig.get_color_interop_id(colorspace); + + return (interop_id == "srgb_rec709_scene" + || interop_id == "srgb_rec709_display"); +} + +std::vector +get_colorspace_icc_profile(const ImageSpec& spec, bool /*from_colorspace*/) +{ + std::vector icc_profile; + const ParamValue* p = spec.find_attribute("ICCProfile"); + if (p) { + cspan icc_profile_span = p->as_cspan(); + icc_profile.assign(icc_profile_span.begin(), icc_profile_span.end()); + } + return icc_profile; +} + +cspan +get_colorspace_cicp(const ImageSpec& spec, bool from_colorspace) +{ + const ParamValue* p = spec.find_attribute("CICP", + TypeDesc(TypeDesc::INT, 4)); + if (p) + return p->as_cspan(); + if (!from_colorspace) + return cspan(); + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + return colorconfig.get_cicp(spec.get_string_attribute("oiio:ColorSpace")); +} -OIIO_NAMESPACE_END +OIIO_NAMESPACE_3_1_END diff --git a/src/png.imageio/png_pvt.h b/src/png.imageio/png_pvt.h index 13700666a5..cadc0eae07 100644 --- a/src/png.imageio/png_pvt.h +++ b/src/png.imageio/png_pvt.h @@ -39,7 +39,6 @@ For further information see the following mailing list threads: OIIO_PLUGIN_NAMESPACE_BEGIN -#define ICC_PROFILE_ATTR "ICCProfile" #define CICP_ATTR "CICP" namespace PNG_pvt { @@ -635,85 +634,36 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, convert_alpha = spec.alpha_channel != -1 && !spec.get_int_attribute("oiio:UnassociatedAlpha", 0); - string_view colorspace = spec.get_string_attribute("oiio:ColorSpace", - "srgb_rec709_scene"); - const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); OIIO_MAYBE_UNUSED bool wrote_colorspace = false; srgb = false; - if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { - srgb = true; + if (is_colorspace_srgb(spec)) { gamma = 1.0f; - } else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) { - gamma = 2.2f; - } else if (colorconfig.equivalent(colorspace, "g24_rec709_scene")) { - gamma = 2.4f; - } else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) { - gamma = 1.8f; - } else { - gamma = spec.get_float_attribute("oiio:Gamma", 1.0f); - // obsolete "oiio:Gamma" attrib for back compatibility - } - - if (colorconfig.equivalent(colorspace, "scene_linear") - || colorconfig.equivalent(colorspace, "lin_rec709_scene")) { - if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) - return "Could not set PNG gAMA chunk"; - png_set_gAMA(sp, ip, 1.0); - srgb = false; - wrote_colorspace = true; - } else if (Strutil::istarts_with(colorspace, "Gamma")) { - // Back compatible, this is DEPRECATED(3.1) - Strutil::parse_word(colorspace); - float g = Strutil::from_string(colorspace); - if (g >= 0.01f && g <= 10.0f /* sanity check */) - gamma = g; - if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) - return "Could not set PNG gAMA chunk"; - png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; - wrote_colorspace = true; - } else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) { - gamma = 2.2f; - if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) - return "Could not set PNG gAMA chunk"; - png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; - wrote_colorspace = true; - } else if (colorconfig.equivalent(colorspace, "g24_rec709_scene")) { - gamma = 2.4f; - if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) - return "Could not set PNG gAMA chunk"; - png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; - wrote_colorspace = true; - } else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) { - gamma = 1.8f; - if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) - return "Could not set PNG gAMA chunk"; - png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; - wrote_colorspace = true; - } else if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { + srgb = true; if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA and cHRM chunk"; png_set_sRGB_gAMA_and_cHRM(sp, ip, PNG_sRGB_INTENT_ABSOLUTE); - srgb = true; wrote_colorspace = true; + } else { + gamma = get_colorspace_rec709_gamma(spec); + if (gamma != 0.0f) { + if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) + return "Could not set PNG gAMA chunk"; + png_set_gAMA(sp, ip, 1.0 / gamma); + srgb = false; + wrote_colorspace = true; + } else { + gamma = 1.0f; + } } // Write ICC profile, if we have anything - const ParamValue* icc_profile_parameter = spec.find_attribute( - ICC_PROFILE_ATTR); - if (icc_profile_parameter != nullptr) { - unsigned int length = icc_profile_parameter->type().size(); + std::vector icc_profile = get_colorspace_icc_profile(spec); + if (icc_profile.size()) { if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG iCCP chunk"; - unsigned char* icc_profile - = (unsigned char*)icc_profile_parameter->data(); - if (icc_profile && length) { - png_set_iCCP(sp, ip, "Embedded Profile", 0, icc_profile, length); - wrote_colorspace = true; - } + png_set_iCCP(sp, ip, "Embedded Profile", 0, icc_profile.data(), + icc_profile.size()); + wrote_colorspace = true; } if (false && !spec.find_attribute("DateTime")) { @@ -771,11 +721,7 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, #ifdef PNG_cICP_SUPPORTED // Only automatically determine CICP from oiio::ColorSpace if we didn't // write colorspace metadata yet. - const ParamValue* p = spec.find_attribute(CICP_ATTR, - TypeDesc(TypeDesc::INT, 4)); - cspan cicp = (p) ? p->as_cspan() - : (!wrote_colorspace) ? colorconfig.get_cicp(colorspace) - : cspan(); + cspan cicp = get_colorspace_cicp(spec, !wrote_colorspace); if (!cicp.empty()) { png_byte vals[4]; for (int i = 0; i < 4; ++i) diff --git a/src/png.imageio/pnginput.cpp b/src/png.imageio/pnginput.cpp index 55afc89ee8..29ba2d9d60 100644 --- a/src/png.imageio/pnginput.cpp +++ b/src/png.imageio/pnginput.cpp @@ -192,22 +192,15 @@ PNGInput::open(const std::string& name, ImageSpec& newspec) return false; } - string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace", - "srgb_rec709_scene"); - const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - m_srgb = false; - if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { + if (is_colorspace_srgb(m_spec)) { m_srgb = true; m_gamma = 1.0f; - } else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) { - m_gamma = 2.2f; - } else if (colorconfig.equivalent(colorspace, "g24_rec709_scene")) { - m_gamma = 2.4f; - } else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) { - m_gamma = 1.8f; } else { - m_gamma = m_spec.get_float_attribute("oiio:Gamma", 1.0f); - // obsolete "oiio:Gamma" attrib for back compatibility + m_srgb = false; + m_gamma = get_colorspace_rec709_gamma(m_spec); + if (m_gamma == 0.0f) { + m_gamma = 1.0f; + } } newspec = spec(); diff --git a/src/python/py_oiio.cpp b/src/python/py_oiio.cpp index 7a2a8d1c67..4c3d2da49b 100644 --- a/src/python/py_oiio.cpp +++ b/src/python/py_oiio.cpp @@ -4,7 +4,9 @@ #include "py_oiio.h" +#include #include +#include #include #if defined(OIIO_PY_BACKEND_NANOBIND) @@ -545,6 +547,38 @@ declare_global_attribute_functions(py_module& m) set_colorspace_rec709_gamma(spec, gamma); }, "spec"_a, "gamma"_a); + m.def( + "is_colorspace_srgb", + [](const ImageSpec& spec, bool default_to_srgb) { + return is_colorspace_srgb(spec, default_to_srgb); + }, + "spec"_a, "default_to_srgb"_a = true); + m.def( + "get_colorspace_rec709_gamma", + [](const ImageSpec& spec) { return get_colorspace_rec709_gamma(spec); }, + "spec"_a); + m.def( + "get_colorspace_icc_profile", + [](const ImageSpec& spec, + bool from_colorspace) -> std::optional { + std::vector icc_profile + = get_colorspace_icc_profile(spec, from_colorspace); + if (icc_profile.empty()) + return std::nullopt; + return py::bytes(reinterpret_cast(icc_profile.data()), + icc_profile.size()); + }, + "spec"_a, "from_colorspace"_a = true); + m.def( + "get_colorspace_cicp", + [](const ImageSpec& spec, + bool from_colorspace) -> std::optional> { + cspan cicp = get_colorspace_cicp(spec, from_colorspace); + if (cicp.empty()) + return std::nullopt; + return std::array({ cicp[0], cicp[1], cicp[2], cicp[3] }); + }, + "spec"_a, "from_colorspace"_a = true); m.def( "equivalent_colorspace", [](const std::string& a, const std::string& b) { diff --git a/src/python/stubs/OpenImageIO/__init__.pyi b/src/python/stubs/OpenImageIO/__init__.pyi index 63de587699..cc41421922 100644 --- a/src/python/stubs/OpenImageIO/__init__.pyi +++ b/src/python/stubs/OpenImageIO/__init__.pyi @@ -1631,6 +1631,9 @@ def attribute(arg0: str, arg1: str, /) -> None: ... def attribute(arg0: str, arg1: TypeDesc | BASETYPE | str, arg2: object, /) -> None: ... def equivalent_colorspace(arg0: str, arg1: str, /) -> bool: ... def get_bytes_attribute(name: str, defaultval: str = ...) -> bytes: ... +def get_colorspace_cicp(spec: ImageSpec, from_colorspace: bool = ...) -> list[int] | None: ... +def get_colorspace_icc_profile(spec: ImageSpec, from_colorspace: bool = ...) -> bytes | None: ... +def get_colorspace_rec709_gamma(spec: ImageSpec) -> float: ... def get_float_attribute(name: str, defaultval: typing.SupportsFloat = ...) -> float: ... def get_int_attribute(name: str, defaultval: typing.SupportsInt = ...) -> int: ... def get_roi(arg0: ImageSpec, /) -> ROI: ... @@ -1639,6 +1642,7 @@ def get_string_attribute(name: str, defaultval: str = ...) -> str: ... def getattribute(arg0: str, arg1: TypeDesc | BASETYPE | str, /) -> typing.Any: ... def geterror(clear: bool = ...) -> str: ... def intersection(arg0: ROI, arg1: ROI, /) -> ROI: ... +def is_colorspace_srgb(spec: ImageSpec, default_to_srgb: bool = ...) -> bool: ... def is_imageio_format_name(name: str) -> bool: ... def set_colorspace(spec: ImageSpec, name: str) -> None: ... def set_colorspace_rec709_gamma(arg0: ImageSpec, arg1: typing.SupportsFloat, /) -> None: ... diff --git a/src/python/stubs/generate_stubs.py b/src/python/stubs/generate_stubs.py index 336acd99f4..35ad5ab088 100644 --- a/src/python/stubs/generate_stubs.py +++ b/src/python/stubs/generate_stubs.py @@ -36,6 +36,9 @@ class OIIOSignatureGenerator(AdvancedSignatureGenerator): # signatures for these special methods include many inaccurate overloads "*.__ne__": "(self, other: object) -> bool", "*.__eq__": "(self, other: object) -> bool", + # stubgen cannot parse the result of these, override below + "*.get_colorspace_cicp": "(spec: ImageSpec, from_colorspace: bool = ...) -> object", + "*.get_colorspace_icc_profile": "(spec: ImageSpec, from_colorspace: bool = ...) -> object", }, arg_type_overrides={ # FIXME: Buffer may in fact be more accurate here @@ -89,6 +92,9 @@ class OIIOSignatureGenerator(AdvancedSignatureGenerator): ("*.ImageBufAlgo.isConstantColor", "*"): "tuple[float, ...] | None", ("*.ImageBufAlgo.color_range_check", "*"): "tuple[int, ...] | None", + ("*.get_colorspace_cicp", "object"): "list[int] | None", + ("*.get_colorspace_icc_profile", "object"): "bytes | None", + ("*.TextureSystem.imagespec", "object"): "ImageSpec | None", ("*.TextureSystem.texture", "tuple"): "tuple[float, ...]", ("*.TextureSystem.texture3d", "tuple"): "tuple[float, ...]", diff --git a/src/rla.imageio/rlaoutput.cpp b/src/rla.imageio/rlaoutput.cpp index b9d0f0c192..2b237cdd52 100644 --- a/src/rla.imageio/rlaoutput.cpp +++ b/src/rla.imageio/rlaoutput.cpp @@ -256,23 +256,9 @@ RLAOutput::open(const std::string& name, const ImageSpec& userspec, // << m_rla.NumOfMatteChannels << " z " << m_rla.NumOfAuxChannels << "\n"; m_rla.Revision = 0xFFFE; - const ColorConfig& colorconfig = ColorConfig::default_colorconfig(); - string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); - if (colorconfig.equivalent(colorspace, "linear") - || colorconfig.equivalent(colorspace, "scene_linear")) - Strutil::safe_strcpy(m_rla.Gamma, "1.0", sizeof(m_rla.Gamma)); - else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) - Strutil::safe_strcpy(m_rla.Gamma, "2.2", sizeof(m_rla.Gamma)); - else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) - Strutil::safe_strcpy(m_rla.Gamma, "1.8", sizeof(m_rla.Gamma)); - else if (colorconfig.equivalent(colorspace, "g24_rec709_scene")) - Strutil::safe_strcpy(m_rla.Gamma, "2.4", sizeof(m_rla.Gamma)); - else if (Strutil::istarts_with(colorspace, "Gamma")) { - Strutil::parse_word(colorspace); - float g = Strutil::from_string(colorspace); - if (!(g >= 0.01f && g <= 10.0f /* sanity check */)) - g = m_spec.get_float_attribute("oiio:Gamma", 1.f); - safe_format_to(m_rla.Gamma, "{:.10}", g); + const float gamma = get_colorspace_rec709_gamma(m_spec); + if (gamma != 0.0f) { + safe_format_to(m_rla.Gamma, "{:.5g}", gamma); } const ParamValue* p; diff --git a/src/targa.imageio/targaoutput.cpp b/src/targa.imageio/targaoutput.cpp index 304035dd02..1554bb45c8 100644 --- a/src/targa.imageio/targaoutput.cpp +++ b/src/targa.imageio/targaoutput.cpp @@ -181,7 +181,9 @@ TGAOutput::open(const std::string& name, const ImageSpec& userspec, m_convert_alpha = m_spec.alpha_channel != -1 && !m_spec.get_int_attribute("oiio:UnassociatedAlpha", 0); - m_gamma = m_spec.get_float_attribute("oiio:Gamma", 1.0); + m_gamma = get_colorspace_rec709_gamma(m_spec); + if (m_gamma == 0.0f) + m_gamma = 1.0f; // prepare and write Targa header tga_header tga; @@ -360,31 +362,13 @@ TGAOutput::write_tga20_data_fields() } // gamma -- two shorts, giving a ratio - const ColorConfig& colorconfig = ColorConfig::default_colorconfig(); - string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); - if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) { - m_gamma = 2.2f; - write(uint16_t(m_gamma * 10.0f)); - write(uint16_t(10)); - } else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) { - m_gamma = 1.8f; - write(uint16_t(m_gamma * 10.0f)); - write(uint16_t(10)); - } else if (colorconfig.equivalent(colorspace, "g24_rec709_scene")) { - m_gamma = 2.4f; - write(uint16_t(m_gamma * 10.0f)); - write(uint16_t(10)); - } else if (Strutil::istarts_with(colorspace, "Gamma")) { - // Extract gamma value from color space, if it's there - Strutil::parse_word(colorspace); - float g = Strutil::from_string(colorspace); - if (g >= 0.01f && g <= 10.0f /* sanity check */) - m_gamma = g; + const float gamma = get_colorspace_rec709_gamma(m_spec); + if (gamma != 0.0f) { // FIXME: invent a smarter way to convert to a vulgar fraction? // NOTE: the spec states that only 1 decimal place of precision // is needed, thus the expansion by 10 // numerator - write(uint16_t(std::round(m_gamma * 10.0f))); + write(uint16_t(std::round(gamma * 10.0f))); write(uint16_t(10)); } else { // just dump two zeros in there diff --git a/src/tiff.imageio/tiffoutput.cpp b/src/tiff.imageio/tiffoutput.cpp index 6f59678ec5..0379adb21d 100644 --- a/src/tiff.imageio/tiffoutput.cpp +++ b/src/tiff.imageio/tiffoutput.cpp @@ -399,9 +399,6 @@ TIFFOutput::supports(string_view feature) const } -#define ICC_PROFILE_ATTR "ICCProfile" - - // Do all elements of vector d have value v? template inline bool @@ -883,18 +880,14 @@ TIFFOutput::open(const std::string& name, const ImageSpec& userspec, } // Write ICC profile, if we have anything - const ParamValue* icc_profile_parameter = m_spec.find_attribute( - ICC_PROFILE_ATTR); - if (icc_profile_parameter != NULL) { - unsigned char* icc_profile - = (unsigned char*)icc_profile_parameter->data(); - uint32_t length = icc_profile_parameter->type().size(); - if (icc_profile && length) - TIFFSetField(m_tif, TIFFTAG_ICCPROFILE, length, icc_profile); - } - - if (equivalent_colorspace(m_spec.get_string_attribute("oiio:ColorSpace"), - "srgb_rec709_scene")) + std::vector icc_profile = get_colorspace_icc_profile(m_spec); + if (icc_profile.size()) { + uint32_t icc_profile_size = uint32_t(icc_profile.size()); + TIFFSetField(m_tif, TIFFTAG_ICCPROFILE, icc_profile_size, + icc_profile.data()); + } + + if (is_colorspace_srgb(m_spec, false)) m_spec.attribute("Exif:ColorSpace", 1); // Deal with missing XResolution or YResolution, or a PixelAspectRatio diff --git a/src/webp.imageio/webpoutput.cpp b/src/webp.imageio/webpoutput.cpp index 784575963e..e96f6e2313 100644 --- a/src/webp.imageio/webpoutput.cpp +++ b/src/webp.imageio/webpoutput.cpp @@ -142,16 +142,8 @@ bool WebpOutput::write_complete_data() { // Check if we have an optional ICC Profile to write. - const unsigned char* icc_data = nullptr; - uint32_t icc_data_length = 0; - bool has_icc_data = false; - const ParamValue* icc_profile_parameter = m_spec.find_attribute( - "ICCProfile"); - if (icc_profile_parameter != nullptr) { - icc_data = (const unsigned char*)icc_profile_parameter->data(); - icc_data_length = icc_profile_parameter->type().size(); - has_icc_data = (icc_data && icc_data_length > 0); - } + std::vector icc_profile = get_colorspace_icc_profile(m_spec); + const bool has_icc_data = icc_profile.size() > 0; // If we have ICC data, encode to memory first. This is required in order // to use the WebPMux assembly API below. @@ -203,7 +195,7 @@ WebpOutput::write_complete_data() WebPData image_data = { wrt.mem, wrt.size }; WebPMuxSetImage(mux, &image_data, false); - WebPData icc_chunk = { icc_data, size_t(icc_data_length) }; + WebPData icc_chunk = { icc_profile.data(), icc_profile.size() }; WebPMuxSetChunk(mux, "ICCP", &icc_chunk, false); WebPData assembly; diff --git a/testsuite/python-oiio/ref/out.txt b/testsuite/python-oiio/ref/out.txt index 97ec265539..21cc18bae5 100644 --- a/testsuite/python-oiio/ref/out.txt +++ b/testsuite/python-oiio/ref/out.txt @@ -26,6 +26,16 @@ Testing set_colorspace module helpers: after set_colorspace(empty): after set_colorspace_rec709_gamma(2.2): g22_rec709_scene +Testing get_colorspace module helpers: + is_colorspace_srgb(unset): True + is_colorspace_srgb(unset, False): False + get_colorspace_rec709_gamma(g22): 2.2 + is_colorspace_srgb(g22): False + get_colorspace_icc_profile(no profile): None + get_colorspace_icc_profile(dummy profile): b'\x00\x01\x02\xfd\xfe\xff' + get_colorspace_cicp(no attribute): None + get_colorspace_cicp(attribute): [9, 16, 9, 1] + Testing global attribute() one-arg: plugin_searchpath str: path/A:path/B threads int: 6 diff --git a/testsuite/python-oiio/src/test_oiio.py b/testsuite/python-oiio/src/test_oiio.py index 6075f875f6..ae382cad5b 100644 --- a/testsuite/python-oiio/src/test_oiio.py +++ b/testsuite/python-oiio/src/test_oiio.py @@ -73,6 +73,28 @@ def version_code(major: int, minor: int, patch: int) -> int: spec.get_string_attribute ("oiio:ColorSpace")) print ("") + print ("Testing get_colorspace module helpers:") + spec = oiio.ImageSpec() + print (" is_colorspace_srgb(unset):", oiio.is_colorspace_srgb (spec)) + print (" is_colorspace_srgb(unset, False):", + oiio.is_colorspace_srgb (spec, False)) + oiio.set_colorspace_rec709_gamma (spec, 2.2) + print (" get_colorspace_rec709_gamma(g22):", + round (oiio.get_colorspace_rec709_gamma (spec), 4)) + print (" is_colorspace_srgb(g22):", oiio.is_colorspace_srgb (spec)) + print (" get_colorspace_icc_profile(no profile):", + oiio.get_colorspace_icc_profile (spec)) + spec.attribute ("ICCProfile", oiio.TypeDesc("uint8[6]"), + bytes([0, 1, 2, 253, 254, 255])) + print (" get_colorspace_icc_profile(dummy profile):", + oiio.get_colorspace_icc_profile (spec)) + print (" get_colorspace_cicp(no attribute):", + oiio.get_colorspace_cicp (spec, False)) + spec.attribute ("CICP", oiio.TypeDesc("int[4]"), (9, 16, 9, 1)) + print (" get_colorspace_cicp(attribute):", + oiio.get_colorspace_cicp (spec, False)) + print ("") + print ("Testing global attribute() one-arg:") oiio.attribute ("plugin_searchpath", "path/A:path/B") print (" plugin_searchpath str:",