Commit 1dade618 for libheif

commit 1dade6187e50a05e870eeb369d7817a2f7c54bf1
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sun Oct 25 17:29:49 2026 +0100

    Separate color profile storage from encoding properties [GHSA-hhg3-fxg7-3gxw]

diff --git a/libheif/context.cc b/libheif/context.cc
index 623634ed..bc5bbcd6 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -786,16 +786,7 @@ Error HeifContext::interpret_heif_file_images()
     for (const auto& prop : properties) {
       auto colr = std::dynamic_pointer_cast<Box_colr>(prop);
       if (colr) {
-        // The ImageItem color-profile setters also add 'colr' properties for
-        // encoding. On read, only populate the description: this property is
-        // already associated with the item in the file.
-        auto profile = colr->get_color_profile();
-        if (auto icc = std::dynamic_pointer_cast<const color_profile_raw>(profile)) {
-          image->ImageDescription::set_color_profile_icc(icc);
-        }
-        else if (auto nclx = std::dynamic_pointer_cast<const color_profile_nclx>(profile)) {
-          image->ImageDescription::set_color_profile_nclx(nclx->get_nclx_color_profile());
-        }
+        image->set_color_profile(colr->get_color_profile());
         continue;
       }

@@ -1117,11 +1108,11 @@ Error HeifContext::interpret_heif_file_images()

       auto tile_img = iter->second;
       if (image->get_color_profile_icc() == nullptr && tile_img->get_color_profile_icc()) {
-        image->ImageDescription::set_color_profile_icc(tile_img->get_color_profile_icc());
+        image->set_color_profile_icc(tile_img->get_color_profile_icc());
       }

       if (!image->has_nclx_color_profile() && tile_img->has_nclx_color_profile()) {
-        image->ImageDescription::set_color_profile_nclx(tile_img->get_color_profile_nclx());
+        image->set_color_profile_nclx(tile_img->get_color_profile_nclx());
       }
     }
   }
diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc
index b1ce0f5c..b8448089 100644
--- a/libheif/image-items/grid.cc
+++ b/libheif/image-items/grid.cc
@@ -985,7 +985,7 @@ Error ImageItem_Grid::add_image_tile(uint32_t tile_x, uint32_t tile_y,
                                         heif_image_input_class_normal,
                                         m_tile_encoding_options->output_nclx_profile);
     for (auto& property : colr_boxes) {
-      add_property(property, is_property_essential(property));
+      add_color_profile_property(property);
     }

     // Add transformative properties
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index c4d80331..949ea782 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -108,6 +108,16 @@ heif_property_id ImageItem::add_property_without_deduplication(const std::shared
 }


+heif_property_id ImageItem::add_color_profile_property(const std::shared_ptr<Box_colr>& property)
+{
+  if (!property) {
+    return 0;
+  }
+
+  return add_property(property, is_property_essential(property));
+}
+
+
 heif_compression_format ImageItem::compression_format_from_fourcc_infe_type(uint32_t type)
 {
   switch (type) {
@@ -417,6 +427,11 @@ Error ImageItem::encode_to_item(HeifContext* ctx,
   // set item properties

   for (auto& propertyBox : codingResult->properties) {
+    if (auto colr = std::dynamic_pointer_cast<Box_colr>(propertyBox)) {
+      add_color_profile_property(colr);
+      continue;
+    }
+
     bool essential = is_property_essential(propertyBox);

     // TODO: can we simply use add_property() ?
@@ -899,19 +914,6 @@ void ImageItem::set_pixel_ratio(uint32_t h, uint32_t v)
 }


-void ImageItem::set_color_profile_nclx(const nclx_profile& profile)
-{
-  ImageDescription::set_color_profile_nclx(profile);
-  add_property(create_colr_box_nclx(), false);
-}
-
-
-void ImageItem::set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile)
-{
-  ImageDescription::set_color_profile_icc(profile);
-  add_property(create_colr_box_icc(), false);
-}
-
 void ImageItem::set_omaf_image_projection(heif_omaf_image_projection projection)
 {
   ImageDescription::set_omaf_image_projection(projection);
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 789025db..f4cd9fe1 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -171,6 +171,10 @@ public:

   heif_property_id add_property_without_deduplication(const std::shared_ptr<Box>& property, bool essential);

+  // Encoding only: attach an already constructed 'colr' property to the file.
+  // Reading a profile and setting ImageDescription metadata must not do this.
+  virtual heif_property_id add_color_profile_property(const std::shared_ptr<Box_colr>& property);
+
   void set_resolution(uint32_t w, uint32_t h)
   {
     m_width = w;
@@ -345,10 +349,6 @@ public:

   void set_pixel_ratio(uint32_t h, uint32_t v) override;

-  void set_color_profile_nclx(const nclx_profile& profile) override;
-
-  void set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile) override;
-
   void set_omaf_image_projection(heif_omaf_image_projection image_projection) override;

   // --- miaf
diff --git a/libheif/image/image_description.h b/libheif/image/image_description.h
index d3e5a6d2..7b2873d8 100644
--- a/libheif/image/image_description.h
+++ b/libheif/image/image_description.h
@@ -171,22 +171,21 @@ public:

   bool has_nclx_color_profile() const;

-  virtual void set_color_profile_nclx(const nclx_profile& profile) { m_color_profile_nclx = profile; }
+  void set_color_profile_nclx(const nclx_profile& profile) { m_color_profile_nclx = profile; }

   nclx_profile get_color_profile_nclx() const { return m_color_profile_nclx; }

   // get the stored nclx fallback or return the default nclx if none is stored
   nclx_profile get_color_profile_nclx_with_fallback() const;

-  virtual void set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile) { m_color_profile_icc = profile; }
+  void set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile) { m_color_profile_icc = profile; }

   bool has_icc_color_profile() const { return m_color_profile_icc != nullptr; }

   const std::shared_ptr<const color_profile_raw>& get_color_profile_icc() const { return m_color_profile_icc; }

-  // This wrapper calls the virtual setters. On an ImageItem, those setters
-  // also add 'colr' properties for encoding. When reading an existing 'colr'
-  // property, use the qualified ImageDescription setters instead.
+  // These setters only store profile metadata. Encoding an ImageItem must add
+  // a 'colr' property explicitly through ImageItem::add_color_profile_property().
   void set_color_profile(const std::shared_ptr<const color_profile>& profile)
   {
     auto icc = std::dynamic_pointer_cast<const color_profile_raw>(profile);
diff --git a/tests/read_color_profile_properties.cc b/tests/read_color_profile_properties.cc
index 6ed16a70..17210acc 100644
--- a/tests/read_color_profile_properties.cc
+++ b/tests/read_color_profile_properties.cc
@@ -26,8 +26,16 @@

 #include "catch_amalgamated.hpp"
 #include "context.h"
+#include "image-items/iden.h"
 #include "image-items/image_item.h"
 #include "test-config.h"
+#include "libheif/heif.h"
+#include "libheif/heif_properties.h"
+
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <vector>


 TEST_CASE("reading a file does not add color profile properties")
@@ -46,3 +54,69 @@ TEST_CASE("reading a file does not add color profile properties")
   REQUIRE(image->has_nclx_color_profile());
   REQUIRE(image->get_color_profile_icc() == nullptr);
 }
+
+
+TEST_CASE("setting an item profile does not add a property")
+{
+  HeifContext ctx;
+  auto id = ctx.get_heif_file()->add_new_image(fourcc("iden"));
+  REQUIRE(id);
+
+  ImageItem_iden image(&ctx, *id);
+  nclx_profile profile;
+  profile.set_sRGB_defaults();
+  image.set_color_profile(std::make_shared<color_profile_nclx>(profile));
+  auto icc = std::make_shared<color_profile_raw>(fourcc("prof"), std::vector<uint8_t>{1, 2, 3, 4});
+  image.set_color_profile_icc(icc);
+
+  REQUIRE(image.has_nclx_color_profile());
+  REQUIRE(image.get_color_profile_icc() == icc);
+  REQUIRE(image.get_all_properties<Box_colr>().empty());
+
+  auto colr = std::make_shared<Box_colr>();
+  colr->set_color_profile(std::make_shared<color_profile_nclx>(profile));
+  REQUIRE(image.add_color_profile_property(colr) != 0);
+  REQUIRE(image.get_all_properties<Box_colr>().size() == 1);
+
+  std::vector<std::shared_ptr<Box>> stored_properties;
+  REQUIRE(!ctx.get_heif_file()->get_properties(*id, stored_properties));
+  REQUIRE(stored_properties.size() == 1);
+  REQUIRE(std::dynamic_pointer_cast<Box_colr>(stored_properties.front()) != nullptr);
+}
+
+
+TEST_CASE("encoding writes an explicit color profile property")
+{
+  if (!heif_have_encoder_for_format(heif_compression_uncompressed)) {
+    SKIP("uncompressed encoder unavailable");
+  }
+
+  heif_image* image = nullptr;
+  REQUIRE(heif_image_create(8, 8, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &image).code == heif_error_Ok);
+  REQUIRE(heif_image_add_plane(image, heif_channel_interleaved, 8, 8, 8).code == heif_error_Ok);
+  size_t stride = 0;
+  uint8_t* pixels = heif_image_get_plane2(image, heif_channel_interleaved, &stride);
+  REQUIRE(pixels != nullptr);
+  for (int y = 0; y < 8; ++y) {
+    std::memset(pixels + y * stride, 128, 8 * 3);
+  }
+
+  const uint8_t icc_data[] = {0x01, 0x02, 0x03, 0x04};
+  REQUIRE(heif_image_set_raw_color_profile(image, "prof", icc_data, sizeof(icc_data)).code == heif_error_Ok);
+
+  heif_context* context = heif_context_alloc();
+  heif_encoder* encoder = nullptr;
+  REQUIRE(heif_context_get_encoder_for_format(context, heif_compression_uncompressed, &encoder).code == heif_error_Ok);
+  heif_image_handle* handle = nullptr;
+  auto err = heif_context_encode_image(context, image, encoder, nullptr, &handle);
+  INFO("encode error: " << err.message);
+  REQUIRE(err.code == heif_error_Ok);
+
+  auto colr_type = static_cast<heif_item_property_type>(heif_fourcc('c', 'o', 'l', 'r'));
+  REQUIRE(heif_item_get_properties_of_type(context, heif_image_handle_get_item_id(handle), colr_type, nullptr, 0) == 1);
+
+  heif_image_handle_release(handle);
+  heif_encoder_release(encoder);
+  heif_context_free(context);
+  heif_image_release(image);
+}