Commit 0b99ba41 for libheif

commit 0b99ba418cdccc905905a64ebb6a1223d1ccd29c
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 27 15:18:27 2026 +0100

    gimi: implement unci component IDs

diff --git a/examples/heif_info.cc b/examples/heif_info.cc
index 46f1eea2..c26a73d9 100644
--- a/examples/heif_info.cc
+++ b/examples/heif_info.cc
@@ -44,6 +44,8 @@
 #include <libheif/heif_experimental.h>
 #include "libheif/heif_sequences.h"
 #include <libheif/heif_text.h>
+#include <libheif/heif_image_handle.h>
+#include <libheif/heif_uncompressed.h>

 #include <fstream>
 #include <iostream>
@@ -778,6 +780,36 @@ int main(int argc, char** argv)
       properties_shown = true;
     }

+    // --- cmpd components
+
+    uint32_t num_cmpd_components = heif_image_handle_get_number_of_cmpd_components(handle);
+    if (num_cmpd_components > 0) {
+      int num_content_ids = heif_image_handle_has_gimi_component_content_ids(handle);
+
+      std::cout << "  components:\n";
+      for (uint32_t i = 0; i < num_cmpd_components; i++) {
+        uint16_t type = heif_image_handle_get_cmpd_component_type(handle, i);
+        std::cout << "    [" << i << "] type: " << type;
+
+        const char* uri = heif_image_handle_get_cmpd_component_type_uri(handle, i);
+        if (uri) {
+          std::cout << " (uri: " << uri << ")";
+          heif_string_release(uri);
+        }
+
+        if (num_content_ids > 0) {
+          const char* content_id = heif_image_handle_get_gimi_component_content_id(handle, i);
+          if (content_id) {
+            std::cout << "  content ID: " << content_id;
+            heif_string_release(content_id);
+          }
+        }
+
+        std::cout << "\n";
+      }
+      properties_shown = true;
+    }
+
 #if HEIF_WITH_OMAF
     // --- OMAF

diff --git a/libheif/api/libheif/heif_image_handle.cc b/libheif/api/libheif/heif_image_handle.cc
index 5423b142..d996a7d0 100644
--- a/libheif/api/libheif/heif_image_handle.cc
+++ b/libheif/api/libheif/heif_image_handle.cc
@@ -21,7 +21,11 @@
 #include "heif_image_handle.h"
 #include "api_structs.h"
 #include "box.h"
+#if WITH_UNCOMPRESSED_CODEC
+#include "codecs/uncompressed/unc_boxes.h"
+#endif
 #include <climits>
+#include <cstring>
 #include <string>
 #include <memory>

@@ -186,3 +190,108 @@ void heif_image_handle_set_gimi_content_id(heif_image_handle* handle, const char
   handle->context->add_property(handle->image->get_id(), gimi_box, false);
   handle->image->set_gimi_sample_content_id(content_id);
 }
+
+
+uint32_t heif_image_handle_get_number_of_cmpd_components(const heif_image_handle* handle)
+{
+#if WITH_UNCOMPRESSED_CODEC
+  if (!handle) {
+    return 0;
+  }
+  auto cmpd = handle->image->get_property<Box_cmpd>();
+  if (!cmpd) {
+    return 0;
+  }
+  return static_cast<uint32_t>(cmpd->get_components().size());
+#else
+  return 0;
+#endif
+}
+
+
+uint16_t heif_image_handle_get_cmpd_component_type(const heif_image_handle* handle, uint32_t component_idx)
+{
+#if WITH_UNCOMPRESSED_CODEC
+  if (!handle) {
+    return 0;
+  }
+  auto cmpd = handle->image->get_property<Box_cmpd>();
+  if (!cmpd) {
+    return 0;
+  }
+  const auto& components = cmpd->get_components();
+  if (component_idx >= components.size()) {
+    return 0;
+  }
+  return components[component_idx].component_type;
+#else
+  return 0;
+#endif
+}
+
+
+const char* heif_image_handle_get_cmpd_component_type_uri(const heif_image_handle* handle, uint32_t component_idx)
+{
+#if WITH_UNCOMPRESSED_CODEC
+  if (!handle) {
+    return nullptr;
+  }
+  auto cmpd = handle->image->get_property<Box_cmpd>();
+  if (!cmpd) {
+    return nullptr;
+  }
+  const auto& components = cmpd->get_components();
+  if (component_idx >= components.size()) {
+    return nullptr;
+  }
+  const auto& uri = components[component_idx].component_type_uri;
+  if (uri.empty()) {
+    return nullptr;
+  }
+  char* uristring = new char[uri.size() + 1];
+  strcpy(uristring, uri.c_str());
+  return uristring;
+#else
+  return nullptr;
+#endif
+}
+
+
+int heif_image_handle_has_gimi_component_content_ids(const heif_image_handle* handle)
+{
+#if WITH_UNCOMPRESSED_CODEC
+  if (!handle) {
+    return 0;
+  }
+  auto box = handle->image->get_property<Box_gimi_component_content_ids>();
+  if (!box) {
+    return 0;
+  }
+  return static_cast<int>(box->get_content_ids().size());
+#else
+  return 0;
+#endif
+}
+
+
+const char* heif_image_handle_get_gimi_component_content_id(const heif_image_handle* handle, uint32_t component_idx)
+{
+#if WITH_UNCOMPRESSED_CODEC
+  if (!handle) {
+    return nullptr;
+  }
+  auto box = handle->image->get_property<Box_gimi_component_content_ids>();
+  if (!box) {
+    return nullptr;
+  }
+  const auto& ids = box->get_content_ids();
+  if (component_idx >= ids.size()) {
+    return nullptr;
+  }
+  char* idstring = new char[ids[component_idx].size() + 1];
+  strcpy(idstring, ids[component_idx].c_str());
+  return idstring;
+#else
+  return nullptr;
+#endif
+}
diff --git a/libheif/api/libheif/heif_image_handle.h b/libheif/api/libheif/heif_image_handle.h
index d14dae02..3b4f811b 100644
--- a/libheif/api/libheif/heif_image_handle.h
+++ b/libheif/api/libheif/heif_image_handle.h
@@ -125,6 +125,37 @@ const char* heif_image_handle_get_gimi_content_id(const heif_image_handle* handl
 LIBHEIF_API
 void heif_image_handle_set_gimi_content_id(heif_image_handle* handle, const char* content_id);

+
+// --- cmpd component queries
+
+// Returns the number of components in the cmpd box, or 0 if no cmpd property exists.
+LIBHEIF_API
+uint32_t heif_image_handle_get_number_of_cmpd_components(const heif_image_handle*);
+
+// Returns the component_type for the given cmpd component index.
+// Returns 0 if out of range or no cmpd property.
+LIBHEIF_API
+uint16_t heif_image_handle_get_cmpd_component_type(const heif_image_handle*, uint32_t component_idx);
+
+// Returns the component_type_uri for the given cmpd component index (component_type >= 0x8000).
+// Returns NULL if the component does not have a URI.
+// The returned string must be freed with heif_string_release().
+LIBHEIF_API
+const char* heif_image_handle_get_cmpd_component_type_uri(const heif_image_handle*, uint32_t component_idx);
+
+
+// --- GIMI component content IDs (handle-level)
+
+// Returns non-zero (count of content IDs) if an ItemComponentContentIDProperty is set, 0 otherwise.
+LIBHEIF_API
+int heif_image_handle_has_gimi_component_content_ids(const heif_image_handle*);
+
+// Returns the GIMI component content ID for the given component index.
+// Returns NULL if no ItemComponentContentIDProperty is set or index is out of range.
+// The returned string must be freed with heif_string_release().
+LIBHEIF_API
+const char* heif_image_handle_get_gimi_component_content_id(const heif_image_handle*, uint32_t component_idx);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libheif/api/libheif/heif_uncompressed.cc b/libheif/api/libheif/heif_uncompressed.cc
index b1169a72..5ed77ef6 100644
--- a/libheif/api/libheif/heif_uncompressed.cc
+++ b/libheif/api/libheif/heif_uncompressed.cc
@@ -763,6 +763,53 @@ heif_image_get_component_X(complex32, heif_complex32)
 heif_image_get_component_X(complex64, heif_complex64)


+// --- GIMI component content IDs
+
+int heif_image_has_component_content_ids(const heif_image* image)
+{
+  if (!image || !image->image) {
+    return 0;
+  }
+  return static_cast<int>(image->image->get_component_content_ids().size());
+}
+
+
+const char* heif_image_get_component_content_id(const heif_image* image, uint32_t component_idx)
+{
+  if (!image || !image->image || !image->image->has_component_content_ids()) {
+    return nullptr;
+  }
+
+  const auto& ids = image->image->get_component_content_ids();
+  if (component_idx >= ids.size()) {
+    return nullptr;
+  }
+
+  char* idstring = new char[ids[component_idx].size() + 1];
+  strcpy(idstring, ids[component_idx].c_str());
+  return idstring;
+}
+
+
+heif_error heif_image_set_component_content_id(heif_image* image,
+                                               uint32_t component_idx,
+                                               const char* content_id)
+{
+  if (!image || !image->image || !content_id) {
+    return heif_error_null_pointer_argument;
+  }
+
+  auto ids = image->image->get_component_content_ids();
+  if (component_idx >= ids.size()) {
+    ids.resize(component_idx + 1);
+  }
+  ids[component_idx] = content_id;
+  image->image->set_component_content_ids(ids);
+
+  return heif_error_success;
+}
+
+
 heif_error heif_context_add_empty_unci_image(heif_context* ctx,
                                                     const heif_unci_image_parameters* parameters,
                                                     const heif_encoding_options* encoding_options,
diff --git a/libheif/api/libheif/heif_uncompressed.h b/libheif/api/libheif/heif_uncompressed.h
index e8ac61b3..ea36071f 100644
--- a/libheif/api/libheif/heif_uncompressed.h
+++ b/libheif/api/libheif/heif_uncompressed.h
@@ -387,6 +387,26 @@ const heif_complex64* heif_image_get_component_complex64_readonly(const heif_ima
 LIBHEIF_API
 heif_complex64* heif_image_get_component_complex64(heif_image*, uint32_t component_idx, size_t* out_stride);

+
+// --- GIMI component content IDs
+
+// Returns non-zero (the number of content IDs) if component content IDs are set, 0 otherwise.
+LIBHEIF_API
+int heif_image_has_component_content_ids(const heif_image*);
+
+// Get a component content ID by cmpd component index.
+// Returns NULL if no content IDs are set or if the index is out of range.
+// The returned string must be freed with heif_string_release().
+LIBHEIF_API
+const char* heif_image_get_component_content_id(const heif_image*, uint32_t component_idx);
+
+// Set a component content ID for a single cmpd component.
+// If the internal array is too small, it will be resized (new entries default to empty strings).
+LIBHEIF_API
+heif_error heif_image_set_component_content_id(heif_image*,
+                                               uint32_t component_idx,
+                                               const char* content_id);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libheif/box.cc b/libheif/box.cc
index 60da3edb..fd3c001d 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -773,6 +773,11 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       else if (hdr.get_uuid_type() == std::vector<uint8_t>{0x26, 0x1e, 0xf3, 0x74, 0x1d, 0x97, 0x5b, 0xba, 0xac, 0xbd, 0x9d, 0x2c, 0x8e, 0xa7, 0x35, 0x22}) {
         box = std::make_shared<Box_gimi_content_id>();
       }
+#if WITH_UNCOMPRESSED_CODEC
+      else if (hdr.get_uuid_type() == std::vector<uint8_t>{0x9d, 0xb9, 0xdd, 0x6e, 0x37, 0x3c, 0x5a, 0x4e, 0x81, 0x10, 0x21, 0xfc, 0x83, 0xa9, 0x11, 0xfd}) {
+        box = std::make_shared<Box_gimi_component_content_ids>();
+      }
+#endif
       else {
         box = std::make_shared<Box_other>(hdr.get_short_type());
       }
@@ -5177,4 +5182,3 @@ std::string Box_gimi_content_id::dump(Indent& indent) const

   return sstr.str();
 }
-
diff --git a/libheif/codecs/uncompressed/unc_boxes.cc b/libheif/codecs/uncompressed/unc_boxes.cc
index e5c56fdc..06296ef1 100644
--- a/libheif/codecs/uncompressed/unc_boxes.cc
+++ b/libheif/codecs/uncompressed/unc_boxes.cc
@@ -1405,3 +1405,58 @@ Error Box_cloc::write(StreamWriter& writer) const

   return Error::Ok;
 }
+
+
+Error Box_gimi_component_content_ids::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  uint32_t number_of_components = range.read32();
+
+  if (limits->max_components && number_of_components > limits->max_components) {
+    std::stringstream sstr;
+    sstr << "GIMI component content IDs box contains " << number_of_components
+         << " components, but security limit is set to " << limits->max_components << " components";
+    return {heif_error_Invalid_input,
+            heif_suberror_Security_limit_exceeded,
+            sstr.str()};
+  }
+
+  for (uint32_t i = 0; i < number_of_components; i++) {
+    if (range.eof()) {
+      return {heif_error_Invalid_input,
+              heif_suberror_End_of_data,
+              "Not enough data for all component content IDs"};
+    }
+    m_content_ids.push_back(range.read_string());
+  }
+
+  return range.get_error();
+}
+
+
+Error Box_gimi_component_content_ids::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  writer.write32(static_cast<uint32_t>(m_content_ids.size()));
+
+  for (const auto& id : m_content_ids) {
+    writer.write(id);
+  }
+
+  prepend_header(writer, box_start);
+
+  return Error::Ok;
+}
+
+
+std::string Box_gimi_component_content_ids::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+  sstr << Box::dump(indent);
+
+  for (size_t i = 0; i < m_content_ids.size(); i++) {
+    sstr << indent << "[" << i << "] content ID: " << m_content_ids[i] << "\n";
+  }
+
+  return sstr.str();
+}
diff --git a/libheif/codecs/uncompressed/unc_boxes.h b/libheif/codecs/uncompressed/unc_boxes.h
index e6878041..d6a06243 100644
--- a/libheif/codecs/uncompressed/unc_boxes.h
+++ b/libheif/codecs/uncompressed/unc_boxes.h
@@ -495,4 +495,45 @@ public:
 };


+/**
+ * GIMI ItemComponentContentIDProperty.
+ *
+ * A UUID-type item property that assigns a unique Content ID string
+ * to each cmpd component of an image item.
+ *
+ * UUID: 9db9dd6e-373c-5a4e-8110-21fc83a911fd
+ */
+class Box_gimi_component_content_ids : public Box
+{
+public:
+  Box_gimi_component_content_ids()
+  {
+    set_uuid_type(std::vector<uint8_t>{0x9d, 0xb9, 0xdd, 0x6e, 0x37, 0x3c, 0x5a, 0x4e,
+                                       0x81, 0x10, 0x21, 0xfc, 0x83, 0xa9, 0x11, 0xfd});
+  }
+
+  bool is_essential() const override { return false; }
+
+  bool is_transformative_property() const override { return false; }
+
+  std::string dump(Indent&) const override;
+
+  const char* debug_box_name() const override { return "GIMI Component Content IDs"; }
+
+  const std::vector<std::string>& get_content_ids() const { return m_content_ids; }
+
+  void set_content_ids(const std::vector<std::string>& ids) { m_content_ids = ids; }
+
+  [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; }
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits*) override;
+
+  Error write(StreamWriter& writer) const override;
+
+private:
+  std::vector<std::string> m_content_ids;
+};
+
+
 #endif //LIBHEIF_UNC_BOXES_H
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index ab3bf239..18ae7f7f 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -37,6 +37,10 @@
 #include "plugin_registry.h"
 #include "security_limits.h"

+#if WITH_UNCOMPRESSED_CODEC
+#include "codecs/uncompressed/unc_boxes.h"
+#endif
+
 #include <limits>
 #include <cassert>
 #include <cstring>
@@ -931,6 +935,15 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
       img->set_gimi_sample_content_id(gimi_content_id->get_content_id());
     }

+#if WITH_UNCOMPRESSED_CODEC
+    // GIMI component content IDs
+
+    auto gimi_comp_ids = get_property<Box_gimi_component_content_ids>();
+    if (gimi_comp_ids) {
+      img->set_component_content_ids(gimi_comp_ids->get_content_ids());
+    }
+#endif
+
 #if HEIF_WITH_OMAF
     // Image projection (OMAF)
     auto prfr = get_property<Box_prfr>();
diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc
index 0ff24ef8..a0e0126c 100644
--- a/libheif/pixelimage.cc
+++ b/libheif/pixelimage.cc
@@ -32,6 +32,10 @@

 #include "codecs/uncompressed/unc_types.h"

+#if WITH_UNCOMPRESSED_CODEC
+#include "codecs/uncompressed/unc_boxes.h"
+#endif
+

 heif_chroma chroma_from_subsampling(int h, int v)
 {
@@ -319,6 +323,14 @@ std::vector<std::shared_ptr<Box>> ImageExtraData::generate_property_boxes(bool g
   }
 #endif

+#if WITH_UNCOMPRESSED_CODEC
+  if (has_component_content_ids()) {
+    auto ccid = std::make_shared<Box_gimi_component_content_ids>();
+    ccid->set_content_ids(get_component_content_ids());
+    properties.push_back(ccid);
+  }
+#endif
+
   return properties;
 }

@@ -1961,6 +1973,10 @@ void HeifPixelImage::forward_all_metadata_from(const std::shared_ptr<const HeifP
     set_gimi_sample_content_id(src_image->get_gimi_sample_content_id());
   }

+  if (src_image->has_component_content_ids()) {
+    set_component_content_ids(src_image->get_component_content_ids());
+  }
+
   if (auto* tai = src_image->get_tai_timestamp()) {
     set_tai_timestamp(tai);
   }
diff --git a/libheif/pixelimage.h b/libheif/pixelimage.h
index 5e53888d..c01f18c5 100644
--- a/libheif/pixelimage.h
+++ b/libheif/pixelimage.h
@@ -204,6 +204,13 @@ public:
   std::string get_gimi_sample_content_id() const { assert(has_gimi_sample_content_id()); return *m_gimi_sample_content_id; }


+  void set_component_content_ids(const std::vector<std::string>& ids) { m_component_content_ids = ids; }
+
+  bool has_component_content_ids() const { return !m_component_content_ids.empty(); }
+
+  const std::vector<std::string>& get_component_content_ids() const { return m_component_content_ids; }
+
+
   // --- bayer pattern

   bool has_bayer_pattern() const { return m_bayer_pattern.has_value(); }
@@ -283,6 +290,8 @@ private:

   std::optional<std::string> m_gimi_sample_content_id;

+  std::vector<std::string> m_component_content_ids;
+
   std::optional<BayerPattern> m_bayer_pattern;

   std::vector<PolarizationPattern> m_polarization_patterns;