Commit 368b2bb1 for libheif

commit 368b2bb15bbe623ed9592edcff2c63ead0ad0d6e
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 27 08:47:31 2026 +0100

    unci: implement sample non-uniformity metadata (snuc)

diff --git a/libheif/api/libheif/heif_uncompressed.cc b/libheif/api/libheif/heif_uncompressed.cc
index 4e129d24..7e48a245 100644
--- a/libheif/api/libheif/heif_uncompressed.cc
+++ b/libheif/api/libheif/heif_uncompressed.cc
@@ -397,6 +397,128 @@ heif_error heif_image_get_sensor_bad_pixels_map_data(const heif_image* image,
 }


+heif_error heif_image_add_sensor_nuc(heif_image* image,
+                                      uint32_t num_component_indices,
+                                      const uint32_t* component_indices,
+                                      int nuc_is_applied,
+                                      uint32_t image_width,
+                                      uint32_t image_height,
+                                      const float* nuc_gains,
+                                      const float* nuc_offsets)
+{
+  if (image == nullptr || nuc_gains == nullptr || nuc_offsets == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  if (num_component_indices > 0 && component_indices == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  if (image_width == 0 || image_height == 0) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "NUC image dimensions must be non-zero."};
+  }
+
+  SensorNonUniformityCorrection nuc;
+  nuc.component_indices.assign(component_indices, component_indices + num_component_indices);
+  nuc.nuc_is_applied = (nuc_is_applied != 0);
+  nuc.image_width = image_width;
+  nuc.image_height = image_height;
+
+  size_t num_pixels = size_t{image_width} * image_height;
+  nuc.nuc_gains.assign(nuc_gains, nuc_gains + num_pixels);
+  nuc.nuc_offsets.assign(nuc_offsets, nuc_offsets + num_pixels);
+
+  image->image->add_sensor_nuc(nuc);
+
+  return heif_error_success;
+}
+
+
+int heif_image_get_number_of_sensor_nucs(const heif_image* image)
+{
+  if (image == nullptr) {
+    return 0;
+  }
+
+  return static_cast<int>(image->image->get_sensor_nuc().size());
+}
+
+
+heif_error heif_image_get_sensor_nuc_info(const heif_image* image,
+                                           int nuc_index,
+                                           uint32_t* out_num_component_indices,
+                                           int* out_nuc_is_applied,
+                                           uint32_t* out_image_width,
+                                           uint32_t* out_image_height)
+{
+  if (image == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  const auto& nucs = image->image->get_sensor_nuc();
+  if (nuc_index < 0 || static_cast<size_t>(nuc_index) >= nucs.size()) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Sensor NUC index out of range."};
+  }
+
+  const auto& n = nucs[nuc_index];
+  if (out_num_component_indices) {
+    *out_num_component_indices = static_cast<uint32_t>(n.component_indices.size());
+  }
+  if (out_nuc_is_applied) {
+    *out_nuc_is_applied = n.nuc_is_applied ? 1 : 0;
+  }
+  if (out_image_width) {
+    *out_image_width = n.image_width;
+  }
+  if (out_image_height) {
+    *out_image_height = n.image_height;
+  }
+
+  return heif_error_success;
+}
+
+
+heif_error heif_image_get_sensor_nuc_data(const heif_image* image,
+                                           int nuc_index,
+                                           uint32_t* out_component_indices,
+                                           float* out_nuc_gains,
+                                           float* out_nuc_offsets)
+{
+  if (image == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  const auto& nucs = image->image->get_sensor_nuc();
+  if (nuc_index < 0 || static_cast<size_t>(nuc_index) >= nucs.size()) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Sensor NUC index out of range."};
+  }
+
+  const auto& n = nucs[nuc_index];
+
+  if (out_component_indices && !n.component_indices.empty()) {
+    std::copy(n.component_indices.begin(), n.component_indices.end(), out_component_indices);
+  }
+
+  size_t num_pixels = size_t{n.image_width} * n.image_height;
+
+  if (out_nuc_gains && !n.nuc_gains.empty()) {
+    std::copy(n.nuc_gains.begin(), n.nuc_gains.begin() + num_pixels, out_nuc_gains);
+  }
+
+  if (out_nuc_offsets && !n.nuc_offsets.empty()) {
+    std::copy(n.nuc_offsets.begin(), n.nuc_offsets.begin() + num_pixels, out_nuc_offsets);
+  }
+
+  return heif_error_success;
+}
+
+
 heif_unci_image_parameters* heif_unci_image_parameters_alloc()
 {
   auto* params = new heif_unci_image_parameters();
diff --git a/libheif/api/libheif/heif_uncompressed.h b/libheif/api/libheif/heif_uncompressed.h
index 2d8bef15..08245a83 100644
--- a/libheif/api/libheif/heif_uncompressed.h
+++ b/libheif/api/libheif/heif_uncompressed.h
@@ -201,6 +201,50 @@ heif_error heif_image_get_sensor_bad_pixels_map_data(const heif_image*,
                                                       struct heif_bad_pixel* out_bad_pixels);


+// --- Sensor non-uniformity correction (ISO 23001-17, Section 6.1.6)
+
+// Add a sensor non-uniformity correction table to an image.
+// component_indices: array of component indices this NUC applies to (may be NULL if num_component_indices == 0,
+//                    meaning it applies to all components).
+// nuc_gains and nuc_offsets: arrays of image_width * image_height float values.
+// Correction equation: y = nuc_gain * x + nuc_offset.
+// Multiple NUC tables can be added (one per distinct component group).
+LIBHEIF_API
+heif_error heif_image_add_sensor_nuc(heif_image*,
+                                      uint32_t num_component_indices,
+                                      const uint32_t* component_indices,
+                                      int nuc_is_applied,
+                                      uint32_t image_width,
+                                      uint32_t image_height,
+                                      const float* nuc_gains,
+                                      const float* nuc_offsets);
+
+// Returns the number of sensor NUC tables on this image (0 if none).
+LIBHEIF_API
+int heif_image_get_number_of_sensor_nucs(const heif_image*);
+
+// Get the sizes of a sensor NUC table (to allocate arrays for the data query).
+LIBHEIF_API
+heif_error heif_image_get_sensor_nuc_info(const heif_image*,
+                                           int nuc_index,
+                                           uint32_t* out_num_component_indices,
+                                           int* out_nuc_is_applied,
+                                           uint32_t* out_image_width,
+                                           uint32_t* out_image_height);
+
+// Get the actual data of a sensor NUC table.
+// Caller must provide pre-allocated arrays:
+//   out_component_indices: num_component_indices entries (may be NULL if num_component_indices == 0)
+//   out_nuc_gains: image_width * image_height entries
+//   out_nuc_offsets: image_width * image_height entries
+LIBHEIF_API
+heif_error heif_image_get_sensor_nuc_data(const heif_image*,
+                                           int nuc_index,
+                                           uint32_t* out_component_indices,
+                                           float* out_nuc_gains,
+                                           float* out_nuc_offsets);
+
+
 // --- 'unci' images

 // This is similar to heif_metadata_compression. We should try to keep the integers compatible, but each enum will just
diff --git a/libheif/box.cc b/libheif/box.cc
index 47edd2bc..9841f142 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -680,6 +680,10 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       box = std::make_shared<Box_sbpm>();
       break;

+    case fourcc("snuc"):
+      box = std::make_shared<Box_snuc>();
+      break;
+
     case fourcc("uncv"):
       box = std::make_shared<Box_uncv>();
       break;
diff --git a/libheif/codecs/uncompressed/unc_boxes.cc b/libheif/codecs/uncompressed/unc_boxes.cc
index c2859e5e..569673c3 100644
--- a/libheif/codecs/uncompressed/unc_boxes.cc
+++ b/libheif/codecs/uncompressed/unc_boxes.cc
@@ -1246,3 +1246,104 @@ Error Box_sbpm::write(StreamWriter& writer) const

   return Error::Ok;
 }
+
+
+Error Box_snuc::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  parse_full_box_header(range);
+
+  if (get_version() != 0) {
+    return unsupported_version_error("snuc");
+  }
+
+  uint32_t component_count = range.read32();
+
+  if (limits->max_components && component_count > limits->max_components) {
+    return {heif_error_Invalid_input,
+            heif_suberror_Security_limit_exceeded,
+            "snuc component_count exceeds security limit."};
+  }
+
+  m_nuc.component_indices.resize(component_count);
+  for (uint32_t i = 0; i < component_count; i++) {
+    m_nuc.component_indices[i] = range.read32();
+  }
+
+  uint8_t flags = range.read8();
+  m_nuc.nuc_is_applied = !!(flags & 0x80);
+
+  m_nuc.image_width = range.read32();
+  m_nuc.image_height = range.read32();
+
+  uint64_t num_pixels = static_cast<uint64_t>(m_nuc.image_width) * m_nuc.image_height;
+
+  if (limits->max_image_size_pixels && num_pixels > limits->max_image_size_pixels) {
+    return {heif_error_Invalid_input,
+            heif_suberror_Security_limit_exceeded,
+            "snuc image dimensions exceed security limit."};
+  }
+
+  m_nuc.nuc_gains.resize(num_pixels);
+  for (uint64_t i = 0; i < num_pixels; i++) {
+    m_nuc.nuc_gains[i] = range.read_float32();
+  }
+
+  m_nuc.nuc_offsets.resize(num_pixels);
+  for (uint64_t i = 0; i < num_pixels; i++) {
+    m_nuc.nuc_offsets[i] = range.read_float32();
+  }
+
+  return range.get_error();
+}
+
+
+std::string Box_snuc::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+
+  sstr << FullBox::dump(indent);
+
+  sstr << indent << "component_count: " << m_nuc.component_indices.size() << "\n";
+  for (size_t i = 0; i < m_nuc.component_indices.size(); i++) {
+    sstr << indent << "  component_index[" << i << "]: " << m_nuc.component_indices[i] << "\n";
+  }
+
+  sstr << indent << "nuc_is_applied: " << m_nuc.nuc_is_applied << "\n";
+  sstr << indent << "image_width: " << m_nuc.image_width << "\n";
+  sstr << indent << "image_height: " << m_nuc.image_height << "\n";
+
+  uint64_t num_pixels = static_cast<uint64_t>(m_nuc.image_width) * m_nuc.image_height;
+  sstr << indent << "nuc_gains: " << num_pixels << " values\n";
+  sstr << indent << "nuc_offsets: " << num_pixels << " values\n";
+
+  return sstr.str();
+}
+
+
+Error Box_snuc::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  writer.write32(static_cast<uint32_t>(m_nuc.component_indices.size()));
+  for (uint32_t idx : m_nuc.component_indices) {
+    writer.write32(idx);
+  }
+
+  uint8_t flags = m_nuc.nuc_is_applied ? 0x80 : 0;
+  writer.write8(flags);
+
+  writer.write32(m_nuc.image_width);
+  writer.write32(m_nuc.image_height);
+
+  for (float gain : m_nuc.nuc_gains) {
+    writer.write_float32(gain);
+  }
+
+  for (float offset : m_nuc.nuc_offsets) {
+    writer.write_float32(offset);
+  }
+
+  prepend_header(writer, box_start);
+
+  return Error::Ok;
+}
diff --git a/libheif/codecs/uncompressed/unc_boxes.h b/libheif/codecs/uncompressed/unc_boxes.h
index 5457ea3b..2ef0d5dc 100644
--- a/libheif/codecs/uncompressed/unc_boxes.h
+++ b/libheif/codecs/uncompressed/unc_boxes.h
@@ -430,6 +430,32 @@ protected:
 };


+/**
+ * Sensor non-uniformity correction box (snuc).
+ *
+ * Provides per-pixel gain and offset tables for sensor non-uniformity
+ * correction. The correction equation is: y = nuc_gain * x + nuc_offset.
+ *
+ * This is from ISO/IEC 23001-17 Section 6.1.6.
+ */
+class Box_snuc : public FullBox
+{
+public:
+  Box_snuc() { set_short_type(fourcc("snuc")); }
+
+  const SensorNonUniformityCorrection& get_nuc() const { return m_nuc; }
+  void set_nuc(const SensorNonUniformityCorrection& nuc) { m_nuc = nuc; }
+
+  std::string dump(Indent&) const override;
+  Error write(StreamWriter& writer) const override;
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits* limits) override;
+
+  SensorNonUniformityCorrection m_nuc;
+};
+
+
 void fill_uncC_and_cmpd_from_profile(const std::shared_ptr<Box_uncC>& uncC,
                                      std::shared_ptr<Box_cmpd>& cmpd);

diff --git a/libheif/codecs/uncompressed/unc_codec.cc b/libheif/codecs/uncompressed/unc_codec.cc
index aaadafcf..97793b85 100644
--- a/libheif/codecs/uncompressed/unc_codec.cc
+++ b/libheif/codecs/uncompressed/unc_codec.cc
@@ -449,6 +449,7 @@ void UncompressedImageCodec::unci_properties::fill_from_image_item(const std::sh
   cpat = image->get_property<Box_cpat>();
   splz = image->get_all_properties<Box_splz>();
   sbpm = image->get_all_properties<Box_sbpm>();
+  snuc = image->get_all_properties<Box_snuc>();
 }


diff --git a/libheif/codecs/uncompressed/unc_codec.h b/libheif/codecs/uncompressed/unc_codec.h
index 83f2d4ef..dffd51fe 100644
--- a/libheif/codecs/uncompressed/unc_codec.h
+++ b/libheif/codecs/uncompressed/unc_codec.h
@@ -66,6 +66,7 @@ public:
     std::shared_ptr<const Box_cpat> cpat;
     std::vector<std::shared_ptr<const Box_splz>> splz;
     std::vector<std::shared_ptr<const Box_sbpm>> sbpm;
+    std::vector<std::shared_ptr<const Box_snuc>> snuc;

     void fill_from_image_item(const std::shared_ptr<const ImageItem>&);
   };
diff --git a/libheif/codecs/uncompressed/unc_decoder.cc b/libheif/codecs/uncompressed/unc_decoder.cc
index 86ac86f6..d2ef130e 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -478,6 +478,10 @@ Result<std::shared_ptr<HeifPixelImage> > unc_decoder::decode_full_image(
     img->add_sensor_bad_pixels_map(sbpm_box->get_bad_pixels_map());
   }

+  for (const auto& snuc_box : properties.snuc) {
+    img->add_sensor_nuc(snuc_box->get_nuc());
+  }
+
   auto decoderResult = unc_decoder_factory::get_unc_decoder(width, height, cmpd, uncC);
   if (!decoderResult) {
     return decoderResult.error();
diff --git a/libheif/codecs/uncompressed/unc_encoder.cc b/libheif/codecs/uncompressed/unc_encoder.cc
index 3d830a6f..b8b5e2db 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -172,6 +172,10 @@ Result<Encoder::CodedImageData> unc_encoder::encode_static(const std::shared_ptr
     codedImageData.properties.push_back(sbpm);
   }

+  for (const auto& snuc : m_snuc) {
+    codedImageData.properties.push_back(snuc);
+  }
+

   // --- encode image

diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index 12301423..ef081c03 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -34,6 +34,7 @@ class Box_cmpd;
 class Box_cpat;
 class Box_splz;
 class Box_sbpm;
+class Box_snuc;
 class HeifPixelImage;

 heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel);
@@ -53,6 +54,7 @@ public:
   std::shared_ptr<Box_cpat> get_cpat() const { return m_cpat; }
   std::vector<std::shared_ptr<Box_splz>> get_splz() const { return m_splz; }
   std::vector<std::shared_ptr<Box_sbpm>> get_sbpm() const { return m_sbpm; }
+  std::vector<std::shared_ptr<Box_snuc>> get_snuc() const { return m_snuc; }


   virtual uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const = 0;
@@ -71,6 +73,7 @@ protected:
   std::shared_ptr<Box_cpat> m_cpat;
   std::vector<std::shared_ptr<Box_splz>> m_splz;
   std::vector<std::shared_ptr<Box_sbpm>> m_sbpm;
+  std::vector<std::shared_ptr<Box_snuc>> m_snuc;
 };


diff --git a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
index f13c437a..fe65afd1 100644
--- a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
@@ -159,6 +159,14 @@ unc_encoder_component_interleave::unc_encoder_component_interleave(const std::sh
       m_sbpm.push_back(sbpm);
     }
   }
+
+  if (image->has_sensor_nuc()) {
+    for (const auto& nuc : image->get_sensor_nuc()) {
+      auto snuc = std::make_shared<Box_snuc>();
+      snuc->set_nuc(nuc);
+      m_snuc.push_back(snuc);
+    }
+  }
 }


diff --git a/libheif/pixelimage.h b/libheif/pixelimage.h
index afda1086..5d1f2855 100644
--- a/libheif/pixelimage.h
+++ b/libheif/pixelimage.h
@@ -67,6 +67,16 @@ struct SensorBadPixelsMap
   std::vector<BadPixel> bad_pixels;
 };

+struct SensorNonUniformityCorrection
+{
+  std::vector<uint32_t> component_indices;  // empty = applies to all components
+  bool nuc_is_applied = false;
+  uint32_t image_width = 0;
+  uint32_t image_height = 0;
+  std::vector<float> nuc_gains;    // image_width * image_height entries
+  std::vector<float> nuc_offsets;  // image_width * image_height entries
+};
+
 heif_chroma chroma_from_subsampling(int h, int v);

 uint32_t chroma_width(uint32_t w, heif_chroma chroma);
@@ -240,6 +250,17 @@ public:
   virtual void add_sensor_bad_pixels_map(const SensorBadPixelsMap& m) { m_sensor_bad_pixels_maps.push_back(m); }


+  // --- sensor non-uniformity correction
+
+  bool has_sensor_nuc() const { return !m_sensor_nuc.empty(); }
+
+  const std::vector<SensorNonUniformityCorrection>& get_sensor_nuc() const { return m_sensor_nuc; }
+
+  virtual void set_sensor_nuc(const std::vector<SensorNonUniformityCorrection>& n) { m_sensor_nuc = n; }
+
+  virtual void add_sensor_nuc(const SensorNonUniformityCorrection& n) { m_sensor_nuc.push_back(n); }
+
+
 #if HEIF_WITH_OMAF
   bool has_omaf_image_projection() const {
     return (m_omaf_image_projection != heif_omaf_image_projection_flat);
@@ -274,6 +295,8 @@ private:

   std::vector<SensorBadPixelsMap> m_sensor_bad_pixels_maps;

+  std::vector<SensorNonUniformityCorrection> m_sensor_nuc;
+
 #if HEIF_WITH_OMAF
   heif_omaf_image_projection m_omaf_image_projection = heif_omaf_image_projection::heif_omaf_image_projection_flat;
 #endif