Commit edb3f30d for libheif

commit edb3f30d67eef4f1f5550d5c15d7f0b8d3ac568a
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 27 09:15:07 2026 +0100

    unci: implement chroma-sample location (cloc)

diff --git a/libheif/api/libheif/heif_uncompressed.cc b/libheif/api/libheif/heif_uncompressed.cc
index 7e48a245..c231a533 100644
--- a/libheif/api/libheif/heif_uncompressed.cc
+++ b/libheif/api/libheif/heif_uncompressed.cc
@@ -519,6 +519,44 @@ heif_error heif_image_get_sensor_nuc_data(const heif_image* image,
 }


+heif_error heif_image_set_chroma_location(heif_image* image, uint8_t chroma_location)
+{
+  if (image == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  if (chroma_location > 6) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Chroma location must be in the range 0-6."};
+  }
+
+  image->image->set_chroma_location(chroma_location);
+
+  return heif_error_success;
+}
+
+
+int heif_image_has_chroma_location(const heif_image* image)
+{
+  if (image == nullptr) {
+    return 0;
+  }
+
+  return image->image->has_chroma_location() ? 1 : 0;
+}
+
+
+uint8_t heif_image_get_chroma_location(const heif_image* image)
+{
+  if (image == nullptr) {
+    return 0;
+  }
+
+  return image->image->get_chroma_location();
+}
+
+
 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 08245a83..df40d051 100644
--- a/libheif/api/libheif/heif_uncompressed.h
+++ b/libheif/api/libheif/heif_uncompressed.h
@@ -245,6 +245,38 @@ heif_error heif_image_get_sensor_nuc_data(const heif_image*,
                                            float* out_nuc_offsets);


+// --- Chroma sample location (ISO 23091-2 / ITU-T H.273 + ISO 23001-17)
+
+typedef enum heif_chroma420_sample_location {
+  // values 0-5 according to ISO 23091-2 / ITU-T H.273
+  heif_chroma420_sample_location_00_05 = 0,
+  heif_chroma420_sample_location_05_05 = 1,
+  heif_chroma420_sample_location_00_00 = 2,
+  heif_chroma420_sample_location_05_00 = 3,
+  heif_chroma420_sample_location_00_10 = 4,
+  heif_chroma420_sample_location_05_10 = 5,
+
+  // value 6 according to ISO 23001-17
+  heif_chroma420_sample_location_00_00_01_00 = 6
+} heif_chroma420_sample_location;
+
+
+// --- Chroma sample location (ISO 23001-17, Section 6.1.4)
+
+// Set the chroma sample location on an image.
+// chroma_location must be in the range 0-6 (see heif_chroma420_sample_location).
+LIBHEIF_API
+heif_error heif_image_set_chroma_location(heif_image*, uint8_t chroma_location);
+
+// Returns non-zero if the image has a chroma sample location set.
+LIBHEIF_API
+int heif_image_has_chroma_location(const heif_image*);
+
+// Returns the chroma sample location (0-6), or 0 if none is set.
+LIBHEIF_API
+uint8_t heif_image_get_chroma_location(const heif_image*);
+
+
 // --- '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 9841f142..60da3edb 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -684,6 +684,10 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       box = std::make_shared<Box_snuc>();
       break;

+    case fourcc("cloc"):
+      box = std::make_shared<Box_cloc>();
+      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 569673c3..063c1038 100644
--- a/libheif/codecs/uncompressed/unc_boxes.cc
+++ b/libheif/codecs/uncompressed/unc_boxes.cc
@@ -1347,3 +1347,61 @@ Error Box_snuc::write(StreamWriter& writer) const

   return Error::Ok;
 }
+
+
+Error Box_cloc::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  parse_full_box_header(range);
+
+  if (get_version() != 0) {
+    return unsupported_version_error("cloc");
+  }
+
+  m_chroma_location = range.read8();
+
+  if (m_chroma_location > 6) {
+    return {heif_error_Invalid_input,
+            heif_suberror_Invalid_parameter_value,
+            "cloc chroma_location value out of range (must be 0-6)."};
+  }
+
+  return range.get_error();
+}
+
+
+std::string Box_cloc::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+
+  sstr << FullBox::dump(indent);
+
+  static const char* location_names[] = {
+    "h=0,   v=0.5",   // 0
+    "h=0.5, v=0.5",   // 1
+    "h=0,   v=0",     // 2
+    "h=0.5, v=0",     // 3
+    "h=0,   v=1",     // 4
+    "h=0.5, v=1",     // 5
+    "Cr:0,0 / Cb:1,0" // 6
+  };
+
+  sstr << indent << "chroma_location: " << static_cast<int>(m_chroma_location);
+  if (m_chroma_location <= 6) {
+    sstr << " (" << location_names[m_chroma_location] << ")";
+  }
+  sstr << "\n";
+
+  return sstr.str();
+}
+
+
+Error Box_cloc::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  writer.write8(m_chroma_location);
+
+  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 2ef0d5dc..e6878041 100644
--- a/libheif/codecs/uncompressed/unc_boxes.h
+++ b/libheif/codecs/uncompressed/unc_boxes.h
@@ -456,6 +456,31 @@ protected:
 };


+/**
+ * Chroma location box (cloc).
+ *
+ * Signals the chroma sample position for subsampled images.
+ *
+ * This is from ISO/IEC 23001-17 Section 6.1.4.
+ */
+class Box_cloc : public FullBox
+{
+public:
+  Box_cloc() { set_short_type(fourcc("cloc")); }
+
+  uint8_t get_chroma_location() const { return m_chroma_location; }
+  void set_chroma_location(uint8_t loc) { m_chroma_location = loc; }
+
+  std::string dump(Indent&) const override;
+  Error write(StreamWriter& writer) const override;
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits* limits) override;
+
+  uint8_t m_chroma_location = 0;
+};
+
+
 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 97793b85..45818d86 100644
--- a/libheif/codecs/uncompressed/unc_codec.cc
+++ b/libheif/codecs/uncompressed/unc_codec.cc
@@ -450,6 +450,7 @@ void UncompressedImageCodec::unci_properties::fill_from_image_item(const std::sh
   splz = image->get_all_properties<Box_splz>();
   sbpm = image->get_all_properties<Box_sbpm>();
   snuc = image->get_all_properties<Box_snuc>();
+  cloc = image->get_property<Box_cloc>();
 }


diff --git a/libheif/codecs/uncompressed/unc_codec.h b/libheif/codecs/uncompressed/unc_codec.h
index dffd51fe..eb08061a 100644
--- a/libheif/codecs/uncompressed/unc_codec.h
+++ b/libheif/codecs/uncompressed/unc_codec.h
@@ -67,6 +67,7 @@ public:
     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;
+    std::shared_ptr<const Box_cloc> cloc;

     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 d2ef130e..34b77bd9 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -482,6 +482,10 @@ Result<std::shared_ptr<HeifPixelImage> > unc_decoder::decode_full_image(
     img->add_sensor_nuc(snuc_box->get_nuc());
   }

+  if (properties.cloc) {
+    img->set_chroma_location(properties.cloc->get_chroma_location());
+  }
+
   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 b8b5e2db..5b652fd6 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -176,6 +176,10 @@ Result<Encoder::CodedImageData> unc_encoder::encode_static(const std::shared_ptr
     codedImageData.properties.push_back(snuc);
   }

+  if (m_cloc) {
+    codedImageData.properties.push_back(m_cloc);
+  }
+

   // --- encode image

diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index ef081c03..0922839c 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -35,6 +35,7 @@ class Box_cpat;
 class Box_splz;
 class Box_sbpm;
 class Box_snuc;
+class Box_cloc;
 class HeifPixelImage;

 heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel);
@@ -55,6 +56,7 @@ public:
   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; }
+  std::shared_ptr<Box_cloc> get_cloc() const { return m_cloc; }


   virtual uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const = 0;
@@ -74,6 +76,7 @@ protected:
   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;
+  std::shared_ptr<Box_cloc> m_cloc;
 };


diff --git a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
index fe65afd1..e8f9727f 100644
--- a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
@@ -167,6 +167,11 @@ unc_encoder_component_interleave::unc_encoder_component_interleave(const std::sh
       m_snuc.push_back(snuc);
     }
   }
+
+  if (image->has_chroma_location()) {
+    m_cloc = std::make_shared<Box_cloc>();
+    m_cloc->set_chroma_location(image->get_chroma_location());
+  }
 }


diff --git a/libheif/pixelimage.h b/libheif/pixelimage.h
index 5d1f2855..471802ce 100644
--- a/libheif/pixelimage.h
+++ b/libheif/pixelimage.h
@@ -98,21 +98,6 @@ bool is_integer_multiple_of_chroma_size(uint32_t width,
 // Returns the list of valid heif_chroma values for a given colorspace.
 std::vector<heif_chroma> get_valid_chroma_values_for_colorspace(heif_colorspace colorspace);

-// TODO: move to public API when used
-enum heif_chroma420_sample_position {
-  // values 0-5 according to ISO 23091-2 / ITU-T H.273
-  heif_chroma420_sample_position_00_05 = 0,
-  heif_chroma420_sample_position_05_05 = 1,
-  heif_chroma420_sample_position_00_00 = 2,
-  heif_chroma420_sample_position_05_00 = 3,
-  heif_chroma420_sample_position_00_10 = 4,
-  heif_chroma420_sample_position_05_10 = 5,
-
-  // values 6 according to ISO 23001-17
-  heif_chroma420_sample_position_00_00_01_00 = 6
-};
-
-
 class ImageExtraData
 {
 public:
@@ -261,6 +246,15 @@ public:
   virtual void add_sensor_nuc(const SensorNonUniformityCorrection& n) { m_sensor_nuc.push_back(n); }


+  // --- chroma sample location (ISO 23001-17, Section 6.1.4)
+
+  bool has_chroma_location() const { return m_chroma_location.has_value(); }
+
+  uint8_t get_chroma_location() const { return m_chroma_location.value_or(0); }
+
+  virtual void set_chroma_location(uint8_t loc) { m_chroma_location = loc; }
+
+
 #if HEIF_WITH_OMAF
   bool has_omaf_image_projection() const {
     return (m_omaf_image_projection != heif_omaf_image_projection_flat);
@@ -297,6 +291,8 @@ private:

   std::vector<SensorNonUniformityCorrection> m_sensor_nuc;

+  std::optional<uint8_t> m_chroma_location;
+
 #if HEIF_WITH_OMAF
   heif_omaf_image_projection m_omaf_image_projection = heif_omaf_image_projection::heif_omaf_image_projection_flat;
 #endif