Commit 39fe7e86 for libheif

commit 39fe7e865d0c321fc88258b6bca46d1e9dd72c00
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Tue Feb 10 09:11:14 2026 +0100

    unci: new encoder for packed component interleaving

diff --git a/libheif/CMakeLists.txt b/libheif/CMakeLists.txt
index efa15282..5021d7a7 100644
--- a/libheif/CMakeLists.txt
+++ b/libheif/CMakeLists.txt
@@ -317,6 +317,8 @@ if (WITH_UNCOMPRESSED_CODEC)
             codecs/uncompressed/unc_encoder_rgb3_rgba.h
             codecs/uncompressed/unc_encoder_rrggbb.cc
             codecs/uncompressed/unc_encoder_rrggbb.h
+            codecs/uncompressed/unc_encoder_packed_planar.cc
+            codecs/uncompressed/unc_encoder_packed_planar.h
             codecs/uncompressed/unc_encoder_planar.cc
             codecs/uncompressed/unc_encoder_planar.h
             codecs/uncompressed/unc_encoder_rgb_hdr_packed_interleave.cc
diff --git a/libheif/codecs/uncompressed/unc_decoder.cc b/libheif/codecs/uncompressed/unc_decoder.cc
index 7d74d59a..4ea7db02 100644
--- a/libheif/codecs/uncompressed/unc_decoder.cc
+++ b/libheif/codecs/uncompressed/unc_decoder.cc
@@ -357,7 +357,7 @@ Result<std::unique_ptr<unc_decoder>> unc_decoder_factory::get_unc_decoder(
   }

   std::stringstream sstr;
-  sstr << "Uncompressed interleave_type of " << ((int) uncC->get_interleave_type()) << " is not implemented yet";
+  sstr << "No decoder found for uncompressed format (interleave_type of " << ((int) uncC->get_interleave_type()) << ")";
   return Error{heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, sstr.str()};
 }

diff --git a/libheif/codecs/uncompressed/unc_encoder.cc b/libheif/codecs/uncompressed/unc_encoder.cc
index a0c1d1f3..cb1b3112 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -20,10 +20,12 @@

 #include "unc_encoder.h"

+#include <cassert>
 #include <cstring>

 #include "pixelimage.h"
 #include "unc_boxes.h"
+#include "unc_encoder_packed_planar.h"
 #include "unc_encoder_planar.h"
 #include "unc_encoder_rgb_hdr_packed_interleave.h"
 #include "unc_encoder_rgb3_rgba.h"
@@ -31,6 +33,27 @@
 #include "libheif/heif_uncompressed.h"


+heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel)
+{
+  switch (channel) {
+    case heif_channel_Y: return heif_uncompressed_component_type::component_type_Y;
+    case heif_channel_Cb: return heif_uncompressed_component_type::component_type_Cb;
+    case heif_channel_Cr: return heif_uncompressed_component_type::component_type_Cr;
+    case heif_channel_R: return heif_uncompressed_component_type::component_type_red;
+    case heif_channel_G: return heif_uncompressed_component_type::component_type_green;
+    case heif_channel_B: return heif_uncompressed_component_type::component_type_blue;
+    case heif_channel_Alpha: return heif_uncompressed_component_type::component_type_alpha;
+    case heif_channel_interleaved: assert(false);
+      break;
+    case heif_channel_filter_array: return heif_uncompressed_component_type::component_type_filter_array;
+    case heif_channel_depth: return heif_uncompressed_component_type::component_type_depth;
+    case heif_channel_disparity: return heif_uncompressed_component_type::component_type_disparity;
+  }
+
+  return heif_uncompressed_component_type::component_type_padded;
+}
+
+
 heif_uncompressed_component_format to_unc_component_format(heif_channel_datatype channel_datatype)
 {
   switch (channel_datatype) {
@@ -64,12 +87,14 @@ Result<std::unique_ptr<const unc_encoder> > unc_encoder_factory::get_unc_encoder
   static unc_encoder_factory_rgb3_rgba enc_rgb3_rgba;
   static unc_encoder_factory_rgb_hdr_packed_interleave enc_rgb10_12;
   static unc_encoder_factory_rrggbb enc_rrggbb;
+  static unc_encoder_factory_packed_planar enc_packed_planar;
   static unc_encoder_factory_planar enc_planar;

   static const unc_encoder_factory* encoders[]{
     &enc_rgb3_rgba,
     &enc_rgb10_12,
     &enc_rrggbb,
+    &enc_packed_planar,
     &enc_planar
   };

diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index 9b11f588..e7484186 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -25,11 +25,14 @@
 #include "error.h"
 #include "codecs/encoder.h"
 #include "libheif/heif_encoding.h"
+#include "unc_types.h"

 class Box_uncC;
 class Box_cmpd;
 class HeifPixelImage;

+heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel);
+

 class unc_encoder
 {
diff --git a/libheif/codecs/uncompressed/unc_encoder_packed_planar.cc b/libheif/codecs/uncompressed/unc_encoder_packed_planar.cc
new file mode 100644
index 00000000..a32a4983
--- /dev/null
+++ b/libheif/codecs/uncompressed/unc_encoder_packed_planar.cc
@@ -0,0 +1,196 @@
+/*
+ * 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 "unc_encoder_packed_planar.h"
+
+#include <cstring>
+
+#include "pixelimage.h"
+#include "unc_boxes.h"
+
+
+bool unc_encoder_factory_packed_planar::can_encode(const std::shared_ptr<const HeifPixelImage>& image,
+                                                   const heif_encoding_options& options) const
+{
+  if (image->has_channel(heif_channel_interleaved)) {
+    return false;
+  }
+
+  // Check if any component has non-byte-aligned bpp
+  for (auto channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr,
+                        heif_channel_R, heif_channel_G, heif_channel_B,
+                        heif_channel_Alpha, heif_channel_filter_array,
+                        heif_channel_depth, heif_channel_disparity}) {
+    if (image->has_channel(channel)) {
+      int bpp = image->get_bits_per_pixel(channel);
+      if (bpp % 8 != 0) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+
+std::unique_ptr<const unc_encoder> unc_encoder_factory_packed_planar::create(const std::shared_ptr<const HeifPixelImage>& image,
+                                                                              const heif_encoding_options& options) const
+{
+  return std::make_unique<unc_encoder_packed_planar>(image, options);
+}
+
+
+void unc_encoder_packed_planar::add_channel_if_exists(const std::shared_ptr<const HeifPixelImage>& image, heif_channel channel)
+{
+  if (image->has_channel(channel)) {
+    uint8_t bpp = image->get_bits_per_pixel(channel);
+    m_components.push_back({channel, heif_channel_to_component_type(channel), bpp});
+  }
+}
+
+
+unc_encoder_packed_planar::unc_encoder_packed_planar(const std::shared_ptr<const HeifPixelImage>& image,
+                                                     const heif_encoding_options& options)
+{
+  // Special case for heif_channel_Y:
+  // - if this is a YCbCr image, use component_type_Y,
+  // - otherwise, use component_type_monochrome
+
+  if (image->has_channel(heif_channel_Y)) {
+    uint8_t bpp = image->get_bits_per_pixel(heif_channel_Y);
+    if (image->has_channel(heif_channel_Cb) && image->has_channel(heif_channel_Cr)) {
+      m_components.push_back({heif_channel_Y, heif_uncompressed_component_type::component_type_Y, bpp});
+    }
+    else {
+      m_components.push_back({heif_channel_Y, heif_uncompressed_component_type::component_type_monochrome, bpp});
+    }
+  }
+
+  add_channel_if_exists(image, heif_channel_Cb);
+  add_channel_if_exists(image, heif_channel_Cr);
+  add_channel_if_exists(image, heif_channel_R);
+  add_channel_if_exists(image, heif_channel_G);
+  add_channel_if_exists(image, heif_channel_B);
+  add_channel_if_exists(image, heif_channel_Alpha);
+  add_channel_if_exists(image, heif_channel_filter_array);
+  add_channel_if_exists(image, heif_channel_depth);
+  add_channel_if_exists(image, heif_channel_disparity);
+
+
+  uint16_t index = 0;
+  for (const auto& comp : m_components) {
+    m_cmpd->add_component({comp.component_type});
+    m_uncC->add_component({index, comp.bpp, component_format_unsigned, 0});
+    index++;
+  }
+
+  m_uncC->set_interleave_type(interleave_mode_component);
+  m_uncC->set_components_little_endian(false);
+  m_uncC->set_block_size(0);
+
+  if (image->get_chroma_format() == heif_chroma_420) {
+    m_uncC->set_sampling_type(sampling_mode_420);
+  }
+  else if (image->get_chroma_format() == heif_chroma_422) {
+    m_uncC->set_sampling_type(sampling_mode_422);
+  }
+  else {
+    m_uncC->set_sampling_type(sampling_mode_no_subsampling);
+  }
+}
+
+
+uint64_t unc_encoder_packed_planar::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const
+{
+  uint64_t total = 0;
+  for (const auto& comp : m_components) {
+    uint32_t plane_width = tile_width;
+    uint32_t plane_height = tile_height;
+
+    if (comp.channel == heif_channel_Cb || comp.channel == heif_channel_Cr) {
+      // Adjust for chroma subsampling
+      if (m_uncC->get_sampling_type() == sampling_mode_420) {
+        plane_width = (plane_width + 1) / 2;
+        plane_height = (plane_height + 1) / 2;
+      }
+      else if (m_uncC->get_sampling_type() == sampling_mode_422) {
+        plane_width = (plane_width + 1) / 2;
+      }
+    }
+
+    uint64_t row_bytes = (static_cast<uint64_t>(plane_width) * comp.bpp + 7) / 8;
+    total += row_bytes * plane_height;
+  }
+  return total;
+}
+
+
+std::vector<uint8_t> unc_encoder_packed_planar::encode_tile(const std::shared_ptr<const HeifPixelImage>& src_image) const
+{
+  uint64_t total_size = compute_tile_data_size_bytes(src_image->get_width(), src_image->get_height());
+  std::vector<uint8_t> data;
+  data.reserve(total_size);
+
+  for (const auto& comp : m_components) {
+    uint32_t plane_width = src_image->get_width(comp.channel);
+    uint32_t plane_height = src_image->get_height(comp.channel);
+    uint8_t bpp = comp.bpp;
+
+    size_t src_stride;
+    const uint8_t* src_data = src_image->get_plane(comp.channel, &src_stride);
+
+    for (uint32_t y = 0; y < plane_height; y++) {
+      const uint8_t* row = src_data + src_stride * y;
+
+      uint64_t accumulator = 0;
+      int accumulated_bits = 0;
+
+      for (uint32_t x = 0; x < plane_width; x++) {
+        uint32_t sample;
+
+        if (bpp <= 8) {
+          sample = row[x];
+        }
+        else if (bpp <= 16) {
+          sample = reinterpret_cast<const uint16_t*>(row)[x];
+        }
+        else {
+          sample = reinterpret_cast<const uint32_t*>(row)[x];
+        }
+
+        accumulator = (accumulator << bpp) | sample;
+        accumulated_bits += bpp;
+
+        while (accumulated_bits >= 8) {
+          accumulated_bits -= 8;
+          data.push_back(static_cast<uint8_t>(accumulator >> accumulated_bits));
+          accumulator &= (uint64_t{1} << accumulated_bits) - 1;
+        }
+      }
+
+      // Flush partial byte at row end (pad with zeros in LSBs)
+      if (accumulated_bits > 0) {
+        data.push_back(static_cast<uint8_t>(accumulator << (8 - accumulated_bits)));
+      }
+    }
+  }
+
+  return data;
+}
diff --git a/libheif/codecs/uncompressed/unc_encoder_packed_planar.h b/libheif/codecs/uncompressed/unc_encoder_packed_planar.h
new file mode 100644
index 00000000..9d5ad39a
--- /dev/null
+++ b/libheif/codecs/uncompressed/unc_encoder_packed_planar.h
@@ -0,0 +1,63 @@
+/*
+ * 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_UNC_ENCODER_PACKED_PLANAR_H
+#define LIBHEIF_UNC_ENCODER_PACKED_PLANAR_H
+
+#include "unc_encoder.h"
+#include "unc_types.h"
+
+class unc_encoder_packed_planar : public unc_encoder
+{
+public:
+  unc_encoder_packed_planar(const std::shared_ptr<const HeifPixelImage>& image,
+                            const heif_encoding_options& options);
+
+  uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const override;
+
+  [[nodiscard]] std::vector<uint8_t> encode_tile(const std::shared_ptr<const HeifPixelImage>& image) const override;
+
+private:
+  struct channel_component
+  {
+    heif_channel channel;
+    heif_uncompressed_component_type component_type;
+    uint8_t bpp;
+  };
+
+  std::vector<channel_component> m_components;
+
+  void add_channel_if_exists(const std::shared_ptr<const HeifPixelImage>& image, heif_channel channel);
+};
+
+
+class unc_encoder_factory_packed_planar : public unc_encoder_factory
+{
+public:
+
+private:
+  [[nodiscard]] bool can_encode(const std::shared_ptr<const HeifPixelImage>& image,
+                                const heif_encoding_options& options) const override;
+
+  std::unique_ptr<const unc_encoder> create(const std::shared_ptr<const HeifPixelImage>& image,
+                                            const heif_encoding_options& options) const override;
+};
+
+#endif //LIBHEIF_UNC_ENCODER_PACKED_PLANAR_H
diff --git a/libheif/codecs/uncompressed/unc_encoder_planar.cc b/libheif/codecs/uncompressed/unc_encoder_planar.cc
index 6e65eb03..6bd34c83 100644
--- a/libheif/codecs/uncompressed/unc_encoder_planar.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_planar.cc
@@ -39,27 +39,6 @@ std::unique_ptr<const unc_encoder> unc_encoder_factory_planar::create(const std:
 }


-heif_uncompressed_component_type heif_channel_to_component_type(heif_channel channel)
-{
-  switch (channel) {
-    case heif_channel_Y: return heif_uncompressed_component_type::component_type_Y;
-    case heif_channel_Cb: return heif_uncompressed_component_type::component_type_Cb;
-    case heif_channel_Cr: return heif_uncompressed_component_type::component_type_Cr;
-    case heif_channel_R: return heif_uncompressed_component_type::component_type_red;
-    case heif_channel_G: return heif_uncompressed_component_type::component_type_green;
-    case heif_channel_B: return heif_uncompressed_component_type::component_type_blue;
-    case heif_channel_Alpha: return heif_uncompressed_component_type::component_type_alpha;
-    case heif_channel_interleaved: assert(false);
-      break;
-    case heif_channel_filter_array: return heif_uncompressed_component_type::component_type_filter_array;
-    case heif_channel_depth: return heif_uncompressed_component_type::component_type_depth;
-    case heif_channel_disparity: return heif_uncompressed_component_type::component_type_disparity;
-  }
-
-  return heif_uncompressed_component_type::component_type_padded;
-}
-
-
 void unc_encoder_planar::add_channel_if_exists(const std::shared_ptr<const HeifPixelImage>& image, heif_channel channel)
 {
   if (image->has_channel(channel)) {