Commit 4369d8de for libheif

commit 4369d8de1662e2ddf74ef038b65bef7403f5c0ea
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Wed May 27 11:45:31 2026 +0200

    support 'ndwt' box (#1826)

diff --git a/libheif/api/libheif/heif_color.cc b/libheif/api/libheif/heif_color.cc
index 3145f710..89daa587 100644
--- a/libheif/api/libheif/heif_color.cc
+++ b/libheif/api/libheif/heif_color.cc
@@ -518,6 +518,46 @@ void heif_image_handle_set_mastering_display_colour_volume(const heif_image_hand
 }


+// --- nominal diffuse white ---
+
+
+int heif_image_has_nominal_diffuse_white_luminance(const heif_image* image)
+{
+  return image->image->has_nominal_diffuse_white();
+}
+
+
+uint32_t heif_image_get_nominal_diffuse_white_luminance(const heif_image* image)
+{
+  return image->image->get_nominal_diffuse_white_luminance();
+}
+
+
+void heif_image_set_nominal_diffuse_white_luminance(const heif_image* image, uint32_t luminance)
+{
+  image->image->set_nominal_diffuse_white_luminance(luminance);
+}
+
+
+int heif_image_handle_has_nominal_diffuse_white_luminance(const heif_image_handle* handle)
+{
+  return handle->image->get_property<Box_ndwt>() ? 1 : 0;
+}
+
+
+uint32_t heif_image_handle_get_nominal_diffuse_white_luminance(const heif_image_handle* handle)
+{
+  auto ndwt = handle->image->get_property<Box_ndwt>();
+  return ndwt ? ndwt->get_diffuse_white_luminance() : 0;
+}
+
+
+void heif_image_handle_set_nominal_diffuse_white_luminance(const heif_image_handle* handle, uint32_t luminance)
+{
+  handle->image->set_nominal_diffuse_white_luminance(luminance);
+}
+
+
 float mdcv_coord_decode_x(uint16_t coord)
 {
   // check for unspecified value
diff --git a/libheif/api/libheif/heif_color.h b/libheif/api/libheif/heif_color.h
index 2c8953fe..ed798eb2 100644
--- a/libheif/api/libheif/heif_color.h
+++ b/libheif/api/libheif/heif_color.h
@@ -349,6 +349,34 @@ LIBHEIF_API
 void heif_image_handle_set_mastering_display_colour_volume(const heif_image_handle*, const heif_mastering_display_colour_volume* in);


+// --- nominal diffuse white ---
+
+// Nominal diffuse white luminance (ISO/IEC 23008-12 'ndwt' box). The luminance
+// is given in units of 0.0001 candelas per square metre. A value of 0 is valid
+// and selects the default definition of ISO/TS 22028-5.
+
+// If the image has no 'nominal diffuse white' information, the getter returns 0,
+// which is indistinguishable from a stored luminance of 0. Use the has_ function
+// to disambiguate.
+LIBHEIF_API
+int heif_image_has_nominal_diffuse_white_luminance(const heif_image*);
+
+LIBHEIF_API
+uint32_t heif_image_get_nominal_diffuse_white_luminance(const heif_image*);
+
+LIBHEIF_API
+void heif_image_set_nominal_diffuse_white_luminance(const heif_image*, uint32_t luminance);
+
+LIBHEIF_API
+int heif_image_handle_has_nominal_diffuse_white_luminance(const heif_image_handle*);
+
+LIBHEIF_API
+uint32_t heif_image_handle_get_nominal_diffuse_white_luminance(const heif_image_handle*);
+
+LIBHEIF_API
+void heif_image_handle_set_nominal_diffuse_white_luminance(const heif_image_handle*, uint32_t luminance);
+
+
 // Converts the internal numeric representation of heif_mastering_display_colour_volume to the
 // normalized values, collected in heif_decoded_mastering_display_colour_volume.
 // Values that are out-of-range are decoded to 0, indicating an undefined value (as specified in ISO/IEC 23008-2).
diff --git a/libheif/box.cc b/libheif/box.cc
index 25b8de91..a3350333 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -630,6 +630,10 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       box = std::make_shared<Box_amve>();
       break;

+    case fourcc("ndwt"):
+      box = std::make_shared<Box_ndwt>();
+      break;
+
     case fourcc("cmin"):
       box = std::make_shared<Box_cmin>();
       break;
@@ -2916,6 +2920,43 @@ Error Box_amve::write(StreamWriter& writer) const
 }


+Error Box_ndwt::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  parse_full_box_header(range);
+
+  if (get_version() != 0) {
+    return unsupported_version_error("ndwt");
+  }
+
+  m_diffuse_white_luminance = range.read32();
+
+  return range.get_error();
+}
+
+
+std::string Box_ndwt::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+  sstr << Box::dump(indent);
+
+  sstr << indent << "diffuse_white_luminance: " << m_diffuse_white_luminance << "\n";
+
+  return sstr.str();
+}
+
+
+Error Box_ndwt::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  writer.write32(m_diffuse_white_luminance);
+
+  prepend_header(writer, box_start);
+
+  return Error::Ok;
+}
+
+
 Box_cclv::Box_cclv()
 {
   set_short_type(fourcc("cclv"));
diff --git a/libheif/box.h b/libheif/box.h
index 15d86138..1cbdbf6e 100644
--- a/libheif/box.h
+++ b/libheif/box.h
@@ -1432,6 +1432,36 @@ protected:
 };


+class Box_ndwt : public FullBox
+{
+public:
+  Box_ndwt()
+  {
+    set_short_type(fourcc("ndwt"));
+  }
+
+  // Nominal diffuse white luminance in units of 0.0001 cd/m^2.
+  // A value of 0 means the default definition of ISO/TS 22028-5 should be used.
+  uint32_t get_diffuse_white_luminance() const { return m_diffuse_white_luminance; }
+
+  void set_diffuse_white_luminance(uint32_t luminance) { m_diffuse_white_luminance = luminance; }
+
+  std::string dump(Indent&) const override;
+
+  const char* debug_box_name() const override { return "Nominal Diffuse White"; }
+
+  Error write(StreamWriter& writer) const override;
+
+  [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; }
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits*) override;
+
+private:
+  uint32_t m_diffuse_white_luminance = 0;
+};
+
+
 class Box_cclv : public Box
 {
 public:
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 083b9dff..9479c175 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -838,6 +838,13 @@ void ImageItem::set_mdcv(const heif_mastering_display_colour_volume& mdcv)
 }


+void ImageItem::set_nominal_diffuse_white_luminance(uint32_t luminance)
+{
+  ImageDescription::set_nominal_diffuse_white_luminance(luminance);
+  add_property(create_ndwt_box(), false);
+}
+
+
 void ImageItem::set_pixel_ratio(uint32_t h, uint32_t v)
 {
   ImageDescription::set_pixel_ratio(h, v);
@@ -1158,6 +1165,13 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
       img->set_mdcv(mdcv->mdcv);
     }

+    // NDWT
+
+    auto ndwt = get_property<Box_ndwt>();
+    if (ndwt) {
+      img->set_nominal_diffuse_white_luminance(ndwt->get_diffuse_white_luminance());
+    }
+
     // PASP

     auto pasp = get_property<Box_pasp>();
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 550088b8..91faf1c7 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -329,6 +329,8 @@ public:

   void set_mdcv(const heif_mastering_display_colour_volume& mdcv) override;

+  void set_nominal_diffuse_white_luminance(uint32_t luminance) override;
+
   void set_pixel_ratio(uint32_t h, uint32_t v) override;

   void set_color_profile_nclx(const nclx_profile& profile) override;
diff --git a/libheif/image/image_description.cc b/libheif/image/image_description.cc
index d2beae21..7e19bac0 100644
--- a/libheif/image/image_description.cc
+++ b/libheif/image/image_description.cc
@@ -130,6 +130,7 @@ void ImageDescription::copy_metadata_from(const ImageDescription& other)

   m_clli = other.m_clli;
   m_mdcv = other.m_mdcv;
+  m_nominal_diffuse_white_luminance = other.m_nominal_diffuse_white_luminance;

   heif_tai_timestamp_packet_release(m_tai_timestamp);
   m_tai_timestamp = nullptr;
@@ -196,6 +197,19 @@ std::shared_ptr<Box_mdcv> ImageDescription::create_mdcv_box() const
 }


+std::shared_ptr<Box_ndwt> ImageDescription::create_ndwt_box() const
+{
+  if (!has_nominal_diffuse_white()) {
+    return {};
+  }
+
+  auto ndwt = std::make_shared<Box_ndwt>();
+  ndwt->set_diffuse_white_luminance(get_nominal_diffuse_white_luminance());
+
+  return ndwt;
+}
+
+
 std::shared_ptr<Box_pasp> ImageDescription::create_pasp_box() const
 {
   if (!has_nonsquare_pixel_ratio()) {
@@ -278,6 +292,13 @@ std::vector<std::shared_ptr<Box>> ImageDescription::generate_property_boxes(bool
   }


+  // --- write NDWT property
+
+  if (has_nominal_diffuse_white()) {
+    properties.push_back(create_ndwt_box());
+  }
+
+
   // --- write TAI property

   if (auto* tai = get_tai_timestamp()) {
diff --git a/libheif/image/image_description.h b/libheif/image/image_description.h
index 7f29afd0..41626d71 100644
--- a/libheif/image/image_description.h
+++ b/libheif/image/image_description.h
@@ -239,6 +239,23 @@ public:

   void unset_mdcv() { m_mdcv.reset(); }

+  // --- ndwt (nominal diffuse white)
+
+  // Note: a luminance of 0 is a valid value (it selects the ISO/TS 22028-5
+  // default), so presence is tracked separately from the value via std::optional.
+
+  bool has_nominal_diffuse_white() const { return m_nominal_diffuse_white_luminance.has_value(); }
+
+  // Nominal diffuse white luminance in units of 0.0001 cd/m^2.
+  uint32_t get_nominal_diffuse_white_luminance() const { return m_nominal_diffuse_white_luminance.value_or(0); }
+
+  virtual void set_nominal_diffuse_white_luminance(uint32_t luminance)
+  {
+    m_nominal_diffuse_white_luminance = luminance;
+  }
+
+  void unset_nominal_diffuse_white() { m_nominal_diffuse_white_luminance.reset(); }
+
   virtual Error set_tai_timestamp(const heif_tai_timestamp_packet* tai) {
     delete m_tai_timestamp;

@@ -437,6 +454,7 @@ private:
   uint32_t m_PixelAspectRatio_v = 1;
   heif_content_light_level m_clli{};
   std::optional<heif_mastering_display_colour_volume> m_mdcv;
+  std::optional<uint32_t> m_nominal_diffuse_white_luminance;

   heif_tai_timestamp_packet* m_tai_timestamp = nullptr;

@@ -471,6 +489,8 @@ protected:

   std::shared_ptr<Box_mdcv> create_mdcv_box() const;

+  std::shared_ptr<Box_ndwt> create_ndwt_box() const;
+
   std::shared_ptr<Box_pasp> create_pasp_box() const;

   std::shared_ptr<Box_colr> create_colr_box_nclx() const;
diff --git a/libheif/mini.cc b/libheif/mini.cc
index fd91057e..38a394f7 100644
--- a/libheif/mini.cc
+++ b/libheif/mini.cc
@@ -204,7 +204,7 @@ Error Box_mini::parse(BitstreamRange &range, const heif_security_limits *limits)
     bool cclv_flag = bits.get_flag();
     bool amve_flag = bits.get_flag();
     m_reve_flag = bits.get_flag();
-    m_ndwt_flag = bits.get_flag();
+    bool ndwt_flag = bits.get_flag();

     if (clli_flag)
     {
@@ -280,10 +280,10 @@ Error Box_mini::parse(BitstreamRange &range, const heif_security_limits *limits)
       bits.skip_bits(16);
     }

-    if (m_ndwt_flag)
+    if (ndwt_flag)
     {
-      // TODO: NominalDiffuseWhite isn't published yet
-      bits.skip_bits(32);
+      m_ndwt = std::make_shared<Box_ndwt>();
+      m_ndwt->set_diffuse_white_luminance(bits.get_bits32(32));
     }

     if (m_gainmap_flag)
@@ -293,7 +293,7 @@ Error Box_mini::parse(BitstreamRange &range, const heif_security_limits *limits)
       bool tmap_cclv_flag = bits.get_flag();
       bool tmap_amve_flag = bits.get_flag();
       m_tmap_reve_flag = bits.get_flag();
-      m_tmap_ndwt_flag = bits.get_flag();
+      bool tmap_ndwt_flag = bits.get_flag();

       if (tmap_clli_flag)
       {
@@ -369,10 +369,10 @@ Error Box_mini::parse(BitstreamRange &range, const heif_security_limits *limits)
         bits.skip_bits(16);
       }

-      if (m_tmap_ndwt_flag)
+      if (tmap_ndwt_flag)
       {
-        // TODO: NominalDiffuseWhite isn't published yet
-        bits.skip_bits(32);
+        m_tmap_ndwt = std::make_shared<Box_ndwt>();
+        m_tmap_ndwt->set_diffuse_white_luminance(bits.get_bits32(32));
       }
     }
   }
@@ -704,15 +704,17 @@ std::string Box_mini::dump(Indent &indent) const
     }

     sstr << indent << "reve_flag: " << m_reve_flag << "\n";
-    sstr << indent << "ndwt_flag: " << m_ndwt_flag << "\n";

     if (m_reve_flag)
     {
       // TODO - this isn't published yet
     }
-    if (m_ndwt_flag)
+    if (m_ndwt)
     {
-      // TODO - this isn't published yet
+      sstr << indent << "ndwt.diffuse_white_luminance: " << m_ndwt->get_diffuse_white_luminance() << "\n";
+    }
+    else {
+      sstr << indent << "ndwt: ---\n";
     }

     if (m_gainmap_flag)
@@ -782,15 +784,17 @@ std::string Box_mini::dump(Indent &indent) const
       }

       sstr << indent << "tmap_reve_flag: " << m_tmap_reve_flag << "\n";
-      sstr << indent << "tmap_ndwt_flag: " << m_tmap_ndwt_flag << "\n";

       if (m_tmap_reve_flag)
       {
         // TODO - this isn't published yet
       }
-      if (m_tmap_ndwt_flag)
+      if (m_tmap_ndwt)
       {
-        // TODO - this isn't published yet
+        sstr << indent << "tmap_ndwt.diffuse_white_luminance: " << m_tmap_ndwt->get_diffuse_white_luminance() << "\n";
+      }
+      else {
+        sstr << indent << "tmap_ndwt: ---\n";
       }
     }
   }
@@ -1018,7 +1022,7 @@ Error Box_mini::write(StreamWriter& writer) const
     bits.write_flag(m_cclv != nullptr);
     bits.write_flag(m_amve != nullptr);
     bits.write_flag(m_reve_flag);
-    bits.write_flag(m_ndwt_flag);
+    bits.write_flag(m_ndwt != nullptr);

     if (m_clli) {
       bits.write_bits16(m_clli->clli.max_content_light_level, 16);
@@ -1056,9 +1060,8 @@ Error Box_mini::write(StreamWriter& writer) const
       bits.write_bits16(0, 16);
     }

-    if (m_ndwt_flag) {
-      // TODO: NominalDiffuseWhite isn't published yet — write zero
-      bits.write_bits32(0, 32);
+    if (m_ndwt) {
+      bits.write_bits32(m_ndwt->get_diffuse_white_luminance(), 32);
     }

     // Tmap HDR metadata (if gainmap)
@@ -1068,7 +1071,7 @@ Error Box_mini::write(StreamWriter& writer) const
       bits.write_flag(m_tmap_cclv != nullptr);
       bits.write_flag(m_tmap_amve != nullptr);
       bits.write_flag(m_tmap_reve_flag);
-      bits.write_flag(m_tmap_ndwt_flag);
+      bits.write_flag(m_tmap_ndwt != nullptr);

       if (m_tmap_clli) {
         bits.write_bits16(m_tmap_clli->clli.max_content_light_level, 16);
@@ -1105,8 +1108,8 @@ Error Box_mini::write(StreamWriter& writer) const
         bits.write_bits16(0, 16);
       }

-      if (m_tmap_ndwt_flag) {
-        bits.write_bits32(0, 32);
+      if (m_tmap_ndwt) {
+        bits.write_bits32(m_tmap_ndwt->get_diffuse_white_luminance(), 32);
       }
     }
   }
diff --git a/libheif/mini.h b/libheif/mini.h
index bf2854cc..339a2328 100644
--- a/libheif/mini.h
+++ b/libheif/mini.h
@@ -133,10 +133,12 @@ public:
   void set_mdcv(std::shared_ptr<Box_mdcv> box) { m_mdcv = std::move(box); }
   void set_cclv(std::shared_ptr<Box_cclv> box) { m_cclv = std::move(box); }
   void set_amve(std::shared_ptr<Box_amve> box) { m_amve = std::move(box); }
+  void set_ndwt(std::shared_ptr<Box_ndwt> box) { m_ndwt = std::move(box); }
   void set_tmap_clli(std::shared_ptr<Box_clli> box) { m_tmap_clli = std::move(box); }
   void set_tmap_mdcv(std::shared_ptr<Box_mdcv> box) { m_tmap_mdcv = std::move(box); }
   void set_tmap_cclv(std::shared_ptr<Box_cclv> box) { m_tmap_cclv = std::move(box); }
   void set_tmap_amve(std::shared_ptr<Box_amve> box) { m_tmap_amve = std::move(box); }
+  void set_tmap_ndwt(std::shared_ptr<Box_ndwt> box) { m_tmap_ndwt = std::move(box); }

   std::string dump(Indent &) const override;

@@ -208,22 +210,20 @@ private:
   bool m_tmap_full_range_flag = false;

   bool m_reve_flag = false;
-  bool m_ndwt_flag = false;
   std::shared_ptr<Box_clli> m_clli;
   std::shared_ptr<Box_mdcv> m_mdcv;
   std::shared_ptr<Box_cclv> m_cclv;
   std::shared_ptr<Box_amve> m_amve;
   // std::shared_ptr<Box_reve> m_reve;
-  // std::shared_ptr<Box_ndwt> m_ndwt;
+  std::shared_ptr<Box_ndwt> m_ndwt;

   bool m_tmap_reve_flag = false;
-  bool m_tmap_ndwt_flag = false;
   std::shared_ptr<Box_clli> m_tmap_clli;
   std::shared_ptr<Box_mdcv> m_tmap_mdcv;
   std::shared_ptr<Box_cclv> m_tmap_cclv;
   std::shared_ptr<Box_amve> m_tmap_amve;
   // std::shared_ptr<Box_reve> m_tmap_reve;
-  // std::shared_ptr<Box_ndwt> m_tmap_ndwt;
+  std::shared_ptr<Box_ndwt> m_tmap_ndwt;

   std::vector<uint8_t> m_alpha_item_codec_config;
   std::vector<uint8_t> m_gainmap_item_codec_config;