Commit be56fd40 for libheif

commit be56fd40a8f51e6d78ae0b0bfbb1807df47673c6
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Feb 26 20:13:52 2026 +0100

    unci: add support for polarization pattern (splz)

diff --git a/libheif/api/libheif/heif_security.h b/libheif/api/libheif/heif_security.h
index d95378a0..3760db22 100644
--- a/libheif/api/libheif/heif_security.h
+++ b/libheif/api/libheif/heif_security.h
@@ -45,6 +45,7 @@ typedef struct heif_security_limits
   // in 1.5 GB memory need for YUV-4:2:0 or 4 GB for RGB32.
   uint64_t max_image_size_pixels;
   uint64_t max_number_of_tiles;
+  // Also used for polarization pattern (splz) size limit.
   uint32_t max_bayer_pattern_pixels;
   uint32_t max_items;

diff --git a/libheif/api/libheif/heif_uncompressed.cc b/libheif/api/libheif/heif_uncompressed.cc
index c9b21f98..d40efcd0 100644
--- a/libheif/api/libheif/heif_uncompressed.cc
+++ b/libheif/api/libheif/heif_uncompressed.cc
@@ -25,6 +25,7 @@
 #include "image-items/unc_image.h"

 #include <array>
+#include <cstring>
 #include <memory>
 #include <algorithm>

@@ -105,6 +106,154 @@ heif_error heif_image_get_bayer_pattern(const heif_image* image,
 }


+float heif_polarization_angle_no_filter()
+{
+  uint32_t bits = 0xFFFFFFFF;
+  float f;
+  memcpy(&f, &bits, sizeof(f));
+  return f;
+}
+
+
+int heif_polarization_angle_is_no_filter(float angle)
+{
+  uint32_t bits;
+  memcpy(&bits, &angle, sizeof(bits));
+  return bits == 0xFFFFFFFF;
+}
+
+
+heif_error heif_image_add_polarization_pattern(heif_image* image,
+                                               uint32_t num_component_indices,
+                                               const uint32_t* component_indices,
+                                               uint16_t pattern_width,
+                                               uint16_t pattern_height,
+                                               const float* polarization_angles)
+{
+  if (image == nullptr || polarization_angles == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  if (num_component_indices > 0 && component_indices == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  if (pattern_width == 0 || pattern_height == 0) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Polarization pattern dimensions must be non-zero."};
+  }
+
+  PolarizationPattern pattern;
+  pattern.component_indices.assign(component_indices, component_indices + num_component_indices);
+  pattern.pattern_width = pattern_width;
+  pattern.pattern_height = pattern_height;
+
+  size_t num_pixels = size_t{pattern_width} * pattern_height;
+  pattern.polarization_angles.assign(polarization_angles, polarization_angles + num_pixels);
+
+  image->image->add_polarization_pattern(pattern);
+
+  return heif_error_success;
+}
+
+
+int heif_image_get_number_of_polarization_patterns(const heif_image* image)
+{
+  if (image == nullptr) {
+    return 0;
+  }
+
+  return static_cast<int>(image->image->get_polarization_patterns().size());
+}
+
+
+heif_error heif_image_get_polarization_pattern_info(const heif_image* image,
+                                                    int pattern_index,
+                                                    uint32_t* out_num_component_indices,
+                                                    uint16_t* out_pattern_width,
+                                                    uint16_t* out_pattern_height)
+{
+  if (image == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  const auto& patterns = image->image->get_polarization_patterns();
+  if (pattern_index < 0 || static_cast<size_t>(pattern_index) >= patterns.size()) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Polarization pattern index out of range."};
+  }
+
+  const auto& p = patterns[pattern_index];
+  if (out_num_component_indices) {
+    *out_num_component_indices = static_cast<uint32_t>(p.component_indices.size());
+  }
+  if (out_pattern_width) {
+    *out_pattern_width = p.pattern_width;
+  }
+  if (out_pattern_height) {
+    *out_pattern_height = p.pattern_height;
+  }
+
+  return heif_error_success;
+}
+
+
+heif_error heif_image_get_polarization_pattern_data(const heif_image* image,
+                                                    int pattern_index,
+                                                    uint32_t* out_component_indices,
+                                                    float* out_polarization_angles)
+{
+  if (image == nullptr || out_polarization_angles == nullptr) {
+    return heif_error_null_pointer_argument;
+  }
+
+  const auto& patterns = image->image->get_polarization_patterns();
+  if (pattern_index < 0 || static_cast<size_t>(pattern_index) >= patterns.size()) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "Polarization pattern index out of range."};
+  }
+
+  const auto& p = patterns[pattern_index];
+
+  if (out_component_indices && !p.component_indices.empty()) {
+    std::copy(p.component_indices.begin(), p.component_indices.end(), out_component_indices);
+  }
+
+  size_t num_pixels = size_t{p.pattern_width} * p.pattern_height;
+  std::copy(p.polarization_angles.begin(), p.polarization_angles.begin() + num_pixels, out_polarization_angles);
+
+  return heif_error_success;
+}
+
+
+int heif_image_get_polarization_pattern_index_for_component(const heif_image* image,
+                                                            uint32_t component_index)
+{
+  if (image == nullptr) {
+    return -1;
+  }
+
+  const auto& patterns = image->image->get_polarization_patterns();
+  for (size_t i = 0; i < patterns.size(); i++) {
+    const auto& p = patterns[i];
+    if (p.component_indices.empty()) {
+      // Empty component list means pattern applies to all components.
+      return static_cast<int>(i);
+    }
+    for (uint32_t idx : p.component_indices) {
+      if (idx == component_index) {
+        return static_cast<int>(i);
+      }
+    }
+  }
+
+  return -1;
+}
+
+
 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 3f5cf0cb..c6e680d2 100644
--- a/libheif/api/libheif/heif_uncompressed.h
+++ b/libheif/api/libheif/heif_uncompressed.h
@@ -94,6 +94,64 @@ LIBHEIF_API
 heif_error heif_image_get_bayer_pattern(const heif_image*,
                                         heif_bayer_pattern_pixel* out_patternPixels);

+// --- Polarization pattern (ISO 23001-17, Section 6.1.5)
+
+// Special float value indicating "no polarization filter" at a pattern position.
+// On the wire this is the IEEE 754 bit pattern 0xFFFFFFFF (a signaling NaN).
+// Test with heif_polarization_angle_is_no_filter() below, or with isnan()/std::isnan().
+
+// Returns a float with the 0xFFFFFFFF bit pattern (NaN) representing "no polarization filter".
+LIBHEIF_API
+float heif_polarization_angle_no_filter(void);
+
+// Returns non-zero if the given angle has the "no filter" bit pattern (0xFFFFFFFF).
+LIBHEIF_API
+int heif_polarization_angle_is_no_filter(float angle);
+
+// Add a polarization pattern to an image.
+// component_indices: array of component indices this pattern applies to (may be NULL if num_component_indices == 0,
+//                    meaning the pattern applies to all components).
+// polarization_angles: array of pattern_width * pattern_height float values.
+//                      Each is an angle in degrees [0.0, 360.0), or heif_polarization_angle_no_filter() for "no filter".
+// Multiple patterns can be added (one per distinct component group).
+LIBHEIF_API
+heif_error heif_image_add_polarization_pattern(heif_image*,
+                                               uint32_t num_component_indices,
+                                               const uint32_t* component_indices,
+                                               uint16_t pattern_width,
+                                               uint16_t pattern_height,
+                                               const float* polarization_angles);
+
+// Returns the number of polarization patterns on this image (0 if none).
+LIBHEIF_API
+int heif_image_get_number_of_polarization_patterns(const heif_image*);
+
+// Get the sizes/dimensions of a polarization pattern (to allocate arrays for the data query).
+LIBHEIF_API
+heif_error heif_image_get_polarization_pattern_info(const heif_image*,
+                                                    int pattern_index,
+                                                    uint32_t* out_num_component_indices,
+                                                    uint16_t* out_pattern_width,
+                                                    uint16_t* out_pattern_height);
+
+// Get the actual data of a polarization pattern.
+// Caller must provide pre-allocated arrays:
+//   out_component_indices: num_component_indices entries (may be NULL if num_component_indices == 0)
+//   out_polarization_angles: pattern_width * pattern_height entries
+LIBHEIF_API
+heif_error heif_image_get_polarization_pattern_data(const heif_image*,
+                                                    int pattern_index,
+                                                    uint32_t* out_component_indices,
+                                                    float* out_polarization_angles);
+
+// Find the polarization pattern index that applies to a given component index.
+// Returns the pattern index (>= 0), or -1 if no pattern matches.
+// A pattern with an empty component list (component_count == 0) matches all components.
+LIBHEIF_API
+int heif_image_get_polarization_pattern_index_for_component(const heif_image*,
+                                                            uint32_t component_index);
+
+
 // --- '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 bae957fb..945d1de9 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -672,6 +672,10 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       box = std::make_shared<Box_cpat>();
       break;

+    case fourcc("splz"):
+      box = std::make_shared<Box_splz>();
+      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 26396f6d..0ddcd106 100644
--- a/libheif/codecs/uncompressed/unc_boxes.cc
+++ b/libheif/codecs/uncompressed/unc_boxes.cc
@@ -1023,3 +1023,102 @@ Error Box_cpat::write(StreamWriter& writer) const

   return Error::Ok;
 }
+
+
+Error Box_splz::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  parse_full_box_header(range);
+
+  if (get_version() != 0) {
+    return unsupported_version_error("splz");
+  }
+
+  uint32_t component_count = range.read32();
+  m_pattern.component_indices.resize(component_count);
+  for (uint32_t i = 0; i < component_count; i++) {
+    m_pattern.component_indices[i] = range.read32();
+  }
+
+  m_pattern.pattern_width = range.read16();
+  m_pattern.pattern_height = range.read16();
+
+  if (m_pattern.pattern_width == 0 || m_pattern.pattern_height == 0) {
+    return {heif_error_Invalid_input,
+            heif_suberror_Invalid_parameter_value,
+            "Zero polarization pattern size."};
+  }
+
+  auto max_pattern_size = limits->max_bayer_pattern_pixels;
+  if (max_pattern_size && m_pattern.pattern_height > max_pattern_size / m_pattern.pattern_width) {
+    return {heif_error_Invalid_input,
+            heif_suberror_Security_limit_exceeded,
+            "Maximum polarization pattern size exceeded."};
+  }
+
+  size_t num_pixels = size_t{m_pattern.pattern_width} * m_pattern.pattern_height;
+  m_pattern.polarization_angles.resize(num_pixels);
+
+  for (size_t i = 0; i < num_pixels; i++) {
+    m_pattern.polarization_angles[i] = range.read_float32();
+  }
+
+  return range.get_error();
+}
+
+
+std::string Box_splz::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+
+  sstr << FullBox::dump(indent);
+
+  sstr << indent << "component_count: " << m_pattern.component_indices.size() << "\n";
+  for (size_t i = 0; i < m_pattern.component_indices.size(); i++) {
+    sstr << indent << "  component_index[" << i << "]: " << m_pattern.component_indices[i] << "\n";
+  }
+
+  sstr << indent << "pattern_width: " << m_pattern.pattern_width << "\n";
+  sstr << indent << "pattern_height: " << m_pattern.pattern_height << "\n";
+
+  for (uint16_t y = 0; y < m_pattern.pattern_height; y++) {
+    for (uint16_t x = 0; x < m_pattern.pattern_width; x++) {
+      float angle = m_pattern.polarization_angles[y * m_pattern.pattern_width + x];
+      if (heif_polarization_angle_is_no_filter(angle)) {
+        sstr << indent << "  [" << x << "," << y << "]: no filter\n";
+      }
+      else {
+        sstr << indent << "  [" << x << "," << y << "]: " << angle << " degrees\n";
+      }
+    }
+  }
+
+  return sstr.str();
+}
+
+
+Error Box_splz::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  if (m_pattern.pattern_width * size_t{m_pattern.pattern_height} != m_pattern.polarization_angles.size()) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "incorrect number of polarization pattern angles"};
+  }
+
+  writer.write32(static_cast<uint32_t>(m_pattern.component_indices.size()));
+  for (uint32_t idx : m_pattern.component_indices) {
+    writer.write32(idx);
+  }
+
+  writer.write16(m_pattern.pattern_width);
+  writer.write16(m_pattern.pattern_height);
+
+  for (float angle : m_pattern.polarization_angles) {
+    writer.write_float32(angle);
+  }
+
+  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 013f357d..e2f22962 100644
--- a/libheif/codecs/uncompressed/unc_boxes.h
+++ b/libheif/codecs/uncompressed/unc_boxes.h
@@ -374,6 +374,35 @@ protected:
 };


+/**
+ * Polarization pattern definition box (splz).
+ *
+ * Describes the polarization filter array pattern on an image sensor.
+ * Multiple splz boxes can exist (one per set of components with
+ * different polarization filters).
+ *
+ * This is from ISO/IEC 23001-17 Section 6.1.5.
+ */
+class Box_splz : public FullBox
+{
+public:
+  Box_splz() { set_short_type(fourcc("splz")); }
+
+  const PolarizationPattern& get_pattern() const { return m_pattern; }
+
+  void set_pattern(const PolarizationPattern& pattern) { m_pattern = pattern; }
+
+  std::string dump(Indent&) const override;
+
+  Error write(StreamWriter& writer) const override;
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits* limits) override;
+
+  PolarizationPattern m_pattern;
+};
+
+
 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 f9d54f8c..787a501c 100644
--- a/libheif/codecs/uncompressed/unc_codec.cc
+++ b/libheif/codecs/uncompressed/unc_codec.cc
@@ -447,6 +447,7 @@ void UncompressedImageCodec::unci_properties::fill_from_image_item(const std::sh
   cmpC = image->get_property<Box_cmpC>();
   icef = image->get_property<Box_icef>();
   cpat = image->get_property<Box_cpat>();
+  splz = image->get_all_properties<Box_splz>();
 }


diff --git a/libheif/codecs/uncompressed/unc_codec.h b/libheif/codecs/uncompressed/unc_codec.h
index 9dadbcdf..8ad09922 100644
--- a/libheif/codecs/uncompressed/unc_codec.h
+++ b/libheif/codecs/uncompressed/unc_codec.h
@@ -64,7 +64,7 @@ public:
     std::shared_ptr<const Box_cmpC> cmpC;
     std::shared_ptr<const Box_icef> icef;
     std::shared_ptr<const Box_cpat> cpat;
-    // ...
+    std::vector<std::shared_ptr<const Box_splz>> splz;

     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 eade4360..b47779e0 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -470,6 +470,10 @@ Result<std::shared_ptr<HeifPixelImage> > unc_decoder::decode_full_image(
     img->set_bayer_pattern(pattern);
   }

+  for (const auto& splz_box : properties.splz) {
+    img->add_polarization_pattern(splz_box->get_pattern());
+  }
+
   auto decoderResult = unc_decoder_factory::get_unc_decoder(width, height, cmpd, uncC);
   if (!decoderResult) {
     return decoderResult.error();
diff --git a/libheif/codecs/uncompressed/unc_enc.cc b/libheif/codecs/uncompressed/unc_enc.cc
index 9f9be044..4aa66f19 100644
--- a/libheif/codecs/uncompressed/unc_enc.cc
+++ b/libheif/codecs/uncompressed/unc_enc.cc
@@ -67,6 +67,7 @@ std::shared_ptr<class Box_VisualSampleEntry> Encoder_uncompressed::get_sample_de
       case fourcc("cmpC"):
       case fourcc("icef"):
       case fourcc("cpat"):
+      case fourcc("splz"):
         uncv->append_child_box(prop);
       break;
     }
diff --git a/libheif/codecs/uncompressed/unc_encoder.cc b/libheif/codecs/uncompressed/unc_encoder.cc
index f5ccc0b5..77dc871f 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -164,6 +164,10 @@ Result<Encoder::CodedImageData> unc_encoder::encode_static(const std::shared_ptr
     codedImageData.properties.push_back(m_cpat);
   }

+  for (const auto& splz : m_splz) {
+    codedImageData.properties.push_back(splz);
+  }
+

   // --- encode image

diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index d9565ebf..1187ed72 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -32,6 +32,7 @@
 class Box_uncC;
 class Box_cmpd;
 class Box_cpat;
+class Box_splz;
 class HeifPixelImage;

 heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel);
@@ -49,6 +50,7 @@ public:
   std::shared_ptr<Box_cmpd> get_cmpd() const { return m_cmpd; }
   std::shared_ptr<Box_uncC> get_uncC() const { return m_uncC; }
   std::shared_ptr<Box_cpat> get_cpat() const { return m_cpat; }
+  std::vector<std::shared_ptr<Box_splz>> get_splz() const { return m_splz; }


   virtual uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const = 0;
@@ -65,6 +67,7 @@ protected:
   std::shared_ptr<Box_cmpd> m_cmpd;
   std::shared_ptr<Box_uncC> m_uncC;
   std::shared_ptr<Box_cpat> m_cpat;
+  std::vector<std::shared_ptr<Box_splz>> m_splz;
 };


diff --git a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
index 0b5b2aab..06f48117 100644
--- a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
@@ -143,6 +143,14 @@ unc_encoder_component_interleave::unc_encoder_component_interleave(const std::sh
     m_cpat = std::make_shared<Box_cpat>();
     m_cpat->set_pattern(cpat_pattern);
   }
+
+  if (image->has_polarization_patterns()) {
+    for (const auto& pol : image->get_polarization_patterns()) {
+      auto splz = std::make_shared<Box_splz>();
+      splz->set_pattern(pol);
+      m_splz.push_back(splz);
+    }
+  }
 }


diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 81e28c51..8c0abe8e 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -111,6 +111,18 @@ public:
     return nullptr;
   }

+  template<class BoxType>
+  std::vector<std::shared_ptr<const BoxType>> get_all_properties() const
+  {
+    std::vector<std::shared_ptr<const BoxType>> result;
+    for (auto& property : m_properties) {
+      if (auto box = std::dynamic_pointer_cast<const BoxType>(property)) {
+        result.push_back(box);
+      }
+    }
+    return result;
+  }
+
   heif_property_id add_property(std::shared_ptr<Box> property, bool essential);

   heif_property_id add_property_without_deduplication(std::shared_ptr<Box> property, bool essential);
diff --git a/libheif/pixelimage.h b/libheif/pixelimage.h
index 77f0c128..788248b1 100644
--- a/libheif/pixelimage.h
+++ b/libheif/pixelimage.h
@@ -48,6 +48,15 @@ struct BayerPattern
   std::vector<heif_bayer_pattern_pixel> pixels;
 };

+struct PolarizationPattern
+{
+  std::vector<uint32_t> component_indices;  // empty = applies to all components
+  uint16_t pattern_width = 0;
+  uint16_t pattern_height = 0;
+  std::vector<float> polarization_angles;   // pattern_width * pattern_height entries
+                                            // 0xFFFFFFFF bit-pattern (NaN) = no polarization filter
+};
+
 heif_chroma chroma_from_subsampling(int h, int v);

 uint32_t chroma_width(uint32_t w, heif_chroma chroma);
@@ -198,6 +207,18 @@ public:

   virtual void set_bayer_pattern(const BayerPattern& pattern) { m_bayer_pattern = pattern; }

+
+  // --- polarization pattern
+
+  bool has_polarization_patterns() const { return !m_polarization_patterns.empty(); }
+
+  const std::vector<PolarizationPattern>& get_polarization_patterns() const { return m_polarization_patterns; }
+
+  virtual void set_polarization_patterns(const std::vector<PolarizationPattern>& p) { m_polarization_patterns = p; }
+
+  virtual void add_polarization_pattern(const PolarizationPattern& p) { m_polarization_patterns.push_back(p); }
+
+
 #if HEIF_WITH_OMAF
   bool has_omaf_image_projection() const {
     return (m_omaf_image_projection != heif_omaf_image_projection_flat);
@@ -228,6 +249,8 @@ private:

   std::optional<BayerPattern> m_bayer_pattern;

+  std::vector<PolarizationPattern> m_polarization_patterns;
+
 #if HEIF_WITH_OMAF
   heif_omaf_image_projection m_omaf_image_projection = heif_omaf_image_projection::heif_omaf_image_projection_flat;
 #endif