Commit fa8575cc for libheif

commit fa8575cc76b0f1181f0623d9e03c6b868989538a
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Feb 26 11:45:04 2026 +0100

    add debayering color conversion

diff --git a/examples/heif_gen_bayer.cc b/examples/heif_gen_bayer.cc
index adc17e6e..73106afb 100644
--- a/examples/heif_gen_bayer.cc
+++ b/examples/heif_gen_bayer.cc
@@ -252,7 +252,7 @@ int main(int argc, char* argv[])

   heif_image* bayer_img = nullptr;
   err = heif_image_create(width, height,
-                          heif_colorspace_monochrome,
+                          heif_colorspace_filter_array,
                           heif_chroma_monochrome,
                           &bayer_img);
   if (err.code != heif_error_Ok) {
diff --git a/libheif/CMakeLists.txt b/libheif/CMakeLists.txt
index 4d00325a..ca0a0154 100644
--- a/libheif/CMakeLists.txt
+++ b/libheif/CMakeLists.txt
@@ -172,6 +172,8 @@ set(libheif_sources
         color-conversion/alpha.h
         color-conversion/chroma_sampling.cc
         color-conversion/chroma_sampling.h
+        color-conversion/bayer_bilinear.cc
+        color-conversion/bayer_bilinear.h
         sequences/seq_boxes.h
         sequences/seq_boxes.cc
         sequences/chunk.h
diff --git a/libheif/codecs/uncompressed/unc_codec.cc b/libheif/codecs/uncompressed/unc_codec.cc
index 76ea42bf..f9d54f8c 100644
--- a/libheif/codecs/uncompressed/unc_codec.cc
+++ b/libheif/codecs/uncompressed/unc_codec.cc
@@ -131,7 +131,7 @@ Error UncompressedImageCodec::get_heif_chroma_uncompressed(const std::shared_ptr
   if (componentSet == (1 << heif_uncompressed_component_type_filter_array)) {
     // TODO - we should look up the components
     *out_chroma = heif_chroma_monochrome;
-    *out_colourspace = heif_colorspace_monochrome;
+    *out_colourspace = heif_colorspace_filter_array;
   }

   // TODO: more combinations
diff --git a/libheif/codecs/uncompressed/unc_decoder.cc b/libheif/codecs/uncompressed/unc_decoder.cc
index 78659e6b..eade4360 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -455,7 +455,19 @@ Result<std::shared_ptr<HeifPixelImage> > unc_decoder::decode_full_image(
   auto img = *createImgResult;

   if (properties.cpat) {
-    img->set_bayer_pattern(properties.cpat->get_pattern());
+    // Resolve cpat component indices to actual component types from cmpd.
+    BayerPattern pattern = properties.cpat->get_pattern();
+    const auto& cmpd_components = cmpd->get_components();
+    for (auto& pixel : pattern.pixels) {
+      uint16_t idx = pixel.component_type;
+      if (idx >= cmpd_components.size()) {
+        return Error(heif_error_Invalid_input,
+                     heif_suberror_Invalid_parameter_value,
+                     "cpat component index out of range");
+      }
+      pixel.component_type = cmpd_components[idx].component_type;
+    }
+    img->set_bayer_pattern(pattern);
   }

   auto decoderResult = unc_decoder_factory::get_unc_decoder(width, height, cmpd, uncC);
diff --git a/libheif/color-conversion/bayer_bilinear.cc b/libheif/color-conversion/bayer_bilinear.cc
new file mode 100644
index 00000000..d6d08666
--- /dev/null
+++ b/libheif/color-conversion/bayer_bilinear.cc
@@ -0,0 +1,211 @@
+/*
+ * HEIF codec.
+ * Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "bayer_bilinear.h"
+#include <libheif/heif_uncompressed.h>
+#include <array>
+#include <cassert>
+
+
+std::vector<ColorStateWithCost>
+Op_bayer_bilinear_to_RGB24_32::state_after_conversion(const ColorState& input_state,
+                                             const ColorState& target_state,
+                                             const heif_color_conversion_options& options,
+                                             const heif_color_conversion_options_ext& options_ext) const
+{
+  if (input_state.colorspace != heif_colorspace_filter_array ||
+      input_state.chroma != heif_chroma_monochrome) {
+    return {};
+  }
+
+  std::vector<ColorStateWithCost> states;
+
+  ColorState output_state;
+  output_state.colorspace = heif_colorspace_RGB;
+  output_state.has_alpha = false;
+
+  if (input_state.bits_per_pixel == 8) {
+    output_state.chroma = heif_chroma_interleaved_RGB;
+    output_state.bits_per_pixel = 8;
+  }
+  else if (input_state.bits_per_pixel > 8 && input_state.bits_per_pixel <= 16) {
+    output_state.chroma = heif_chroma_interleaved_RRGGBB_LE;
+    output_state.bits_per_pixel = input_state.bits_per_pixel;
+  }
+  else {
+    return {};
+  }
+
+  states.emplace_back(output_state, SpeedCosts_Unoptimized);
+
+  return states;
+}
+
+
+// Map uncompressed component types to R/G/B output channel indices.
+// Returns -1 for unknown component types.
+static int component_type_to_rgb_index(uint16_t component_type)
+{
+  switch (component_type) {
+    case heif_uncompressed_component_type_red:
+      return 0;
+    case heif_uncompressed_component_type_green:
+      return 1;
+    case heif_uncompressed_component_type_blue:
+      return 2;
+    default:
+      return -1;
+  }
+}
+
+
+Result<std::shared_ptr<HeifPixelImage>>
+Op_bayer_bilinear_to_RGB24_32::convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+                                         const ColorState& input_state,
+                                         const ColorState& target_state,
+                                         const heif_color_conversion_options& options,
+                                         const heif_color_conversion_options_ext& options_ext,
+                                         const heif_security_limits* limits) const
+{
+  uint32_t width = input->get_width();
+  uint32_t height = input->get_height();
+
+  if (!input->has_bayer_pattern()) {
+    return Error::InternalError;
+  }
+
+  const BayerPattern& pattern = input->get_bayer_pattern();
+  uint16_t pw = pattern.pattern_width;
+  uint16_t ph = pattern.pattern_height;
+
+  if (pw == 0 || ph == 0) {
+    return Error::InternalError;
+  }
+
+  int bpp = input->get_bits_per_pixel(heif_channel_filter_array);
+  bool hdr = bpp > 8;
+
+  heif_chroma out_chroma = hdr ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB;
+
+  auto outimg = std::make_shared<HeifPixelImage>();
+
+  outimg->create(width, height, heif_colorspace_RGB, out_chroma);
+
+  if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, bpp, limits)) {
+    return err;
+  }
+
+  size_t in_stride = 0;
+  const uint8_t* in_p = input->get_plane(heif_channel_filter_array, &in_stride);
+
+  size_t out_stride = 0;
+  uint8_t* out_p = outimg->get_plane(heif_channel_interleaved, &out_stride);
+
+  // Build a lookup table: for each pattern position, which RGB channel (0=R,1=G,2=B) does it provide?
+  std::vector<int> pattern_channel(pw * ph);
+  for (int i = 0; i < pw * ph; i++) {
+    pattern_channel[i] = component_type_to_rgb_index(pattern.pixels[i].component_type);
+    if (pattern_channel[i] < 0) {
+      return Error(heif_error_Unsupported_feature,
+                   heif_suberror_Unsupported_data_version,
+                   "Bayer pattern contains component types that we currently cannot convert to RGB");
+    }
+  }
+
+  // Precompute neighbor offset tables for each pattern position and channel.
+  // neighbor_offsets[py * pw + px][ch] = list of (dx, dy) offsets to average.
+  // For the channel this position directly provides: single entry (0, 0).
+  // For other channels: all neighbor offsets within the search radius that provide that channel.
+  std::vector<std::array<std::vector<std::pair<int, int>>, 3>> neighbor_offsets(pw * ph);
+
+  for (int py = 0; py < ph; py++) {
+    for (int px = 0; px < pw; px++) {
+      int this_ch = pattern_channel[py * pw + px];
+      auto& offsets = neighbor_offsets[py * pw + px];
+
+      // The channel this position directly provides: just read from (0,0)
+      offsets[this_ch].emplace_back(0, 0);
+
+      // For the other two channels: collect neighbor offsets
+      int search_radius_x = pw - 1;
+      int search_radius_y = ph - 1;
+
+      for (int dy = -search_radius_y; dy <= search_radius_y; dy++) {
+        for (int dx = -search_radius_x; dx <= search_radius_x; dx++) {
+          if (dx == 0 && dy == 0) {
+            continue;
+          }
+
+          int npx = (((px + dx) % pw) + pw) % pw;
+          int npy = (((py + dy) % ph) + ph) % ph;
+          int neighbor_ch = pattern_channel[npy * pw + npx];
+
+          if (neighbor_ch != this_ch) {
+            offsets[neighbor_ch].emplace_back(dx, dy);
+          }
+        }
+      }
+    }
+  }
+
+  // Bilinear demosaicing using precomputed offset tables.
+  auto demosaic = [&]<typename Pixel>(const Pixel* in, Pixel* out,
+                                      size_t in_str, size_t out_str) {
+    for (uint32_t y = 0; y < height; y++) {
+      for (uint32_t x = 0; x < width; x++) {
+        const auto& offsets = neighbor_offsets[(y % ph) * pw + (x % pw)];
+
+        Pixel* out_pixel = &out[y * out_str + x * 3];
+
+        for (int ch = 0; ch < 3; ch++) {
+          const auto& ch_offsets = offsets[ch];
+          int sum = 0;
+          int count = 0;
+
+          for (const auto& [dx, dy] : ch_offsets) {
+            int nx = static_cast<int>(x) + dx;
+            int ny = static_cast<int>(y) + dy;
+
+            if (nx < 0 || nx >= static_cast<int>(width) ||
+                ny < 0 || ny >= static_cast<int>(height)) {
+              continue;
+            }
+
+            sum += in[ny * in_str + nx];
+            count++;
+          }
+
+          out_pixel[ch] = count > 0 ? static_cast<Pixel>((sum + count / 2) / count) : 0;
+        }
+      }
+    }
+  };
+
+  if (hdr) {
+    demosaic(reinterpret_cast<const uint16_t*>(in_p),
+             reinterpret_cast<uint16_t*>(out_p),
+             in_stride / 2, out_stride / 2);
+  }
+  else {
+    demosaic(in_p, out_p, in_stride, out_stride);
+  }
+
+  return outimg;
+}
diff --git a/libheif/color-conversion/bayer_bilinear.h b/libheif/color-conversion/bayer_bilinear.h
new file mode 100644
index 00000000..53b22853
--- /dev/null
+++ b/libheif/color-conversion/bayer_bilinear.h
@@ -0,0 +1,47 @@
+/*
+ * HEIF codec.
+ * Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
+#define LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
+
+#include "colorconversion.h"
+#include <vector>
+#include <memory>
+
+
+class Op_bayer_bilinear_to_RGB24_32 : public ColorConversionOperation
+{
+public:
+  std::vector<ColorStateWithCost>
+  state_after_conversion(const ColorState& input_state,
+                         const ColorState& target_state,
+                         const heif_color_conversion_options& options,
+                         const heif_color_conversion_options_ext& options_ext) const override;
+
+  Result<std::shared_ptr<HeifPixelImage>>
+  convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+                     const ColorState& input_state,
+                     const ColorState& target_state,
+                     const heif_color_conversion_options& options,
+                     const heif_color_conversion_options_ext& options_ext,
+                     const heif_security_limits* limits) const override;
+};
+
+#endif //LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H
diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index db6b0b5b..0a36ad6b 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -39,6 +39,7 @@
 #include "alpha.h"
 #include "hdr_sdr.h"
 #include "chroma_sampling.h"
+#include "bayer_bilinear.h"

 #if ENABLE_MULTITHREADING_SUPPORT

@@ -67,6 +68,9 @@ std::ostream& operator<<(std::ostream& ostr, heif_colorspace c)
     case heif_colorspace_undefined:
       ostr << "undefined";
       break;
+    case heif_colorspace_filter_array:
+      ostr << "filter_array";
+      break;
     default:
       assert(false);
   }
@@ -230,6 +234,7 @@ void ColorConversionPipeline::init_ops()
   ops.emplace_back(std::make_shared<Op_RGB_to_RRGGBBaa_BE>());
   ops.emplace_back(std::make_shared<Op_mono_to_YCbCr420>());
   ops.emplace_back(std::make_shared<Op_mono_to_RGB24_32>());
+  ops.emplace_back(std::make_shared<Op_bayer_bilinear_to_RGB24_32>());
   ops.emplace_back(std::make_shared<Op_RRGGBBaa_swap_endianness>());
   ops.emplace_back(std::make_shared<Op_RRGGBBaa_BE_to_RGB_HDR>());
   ops.emplace_back(std::make_shared<Op_RGB24_32_to_YCbCr>());
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 0cc6c700..14edd291 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -321,7 +321,7 @@ Result<Encoder::CodedImageData> ImageItem::encode_to_bitstream_and_boxes(const s
   // --- write PIXI property

   std::shared_ptr<Box_pixi> pixi = std::make_shared<Box_pixi>();
-  if (colorspace == heif_colorspace_monochrome && image->has_channel(heif_channel_filter_array)) {
+  if (colorspace == heif_colorspace_filter_array) {
     // Skip pixi for filter array images — bit depth info is in uncC
   }
   else if (colorspace == heif_colorspace_monochrome) {
diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
index 89f2a69d..b166de85 100644
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -158,7 +158,7 @@ Result<std::shared_ptr<ImageItem_uncompressed>> ImageItem_uncompressed::add_unci


   // Add cpat property if Bayer pattern is set
-  if ((*uncEncoder)->get_cpat()) {
+  if (unci_image->m_unc_encoder->get_cpat()) {
     unci_image->add_property((*uncEncoder)->get_cpat(), true);
   }

diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc
index 588e68d7..b7a0201f 100644
--- a/libheif/pixelimage.cc
+++ b/libheif/pixelimage.cc
@@ -388,6 +388,9 @@ std::vector<heif_chroma> get_valid_chroma_values_for_colorspace(heif_colorspace
     case heif_colorspace_nonvisual:
       return {heif_chroma_undefined};

+    case heif_colorspace_filter_array:
+      return {heif_chroma_monochrome};
+
     default:
       return {};
   }