Commit 7de1ebf7 for libheif

commit 7de1ebf74eeabd9159307dc7c00a97aa3b45bafa
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Oct 5 03:55:15 2026 +0200

    Check the bit depth of interleaved formats when a plane is added

    The interleaved chroma formats define the bit depth of their components:
    8 bits for heif_chroma_interleaved_RGB and RGBA, 9 to 16 bits, stored as
    16-bit words, for the RRGGBB formats. add_channel() only refused a bit
    depth of 8 or less for the RRGGBB formats and accepted anything else.

    An interleaved plane with 64-bit or 128-bit components has pixels of more
    than 255 bits. For those, get_storage_bits_per_pixel() ran into its
    assertion (bpp <= 256), and without assertions returned the value cut down
    to 8 bits: 384 bits were reported as 128, and 256 bits as 0, which
    heif_image_get_bits_per_pixel() turned into -1 for a channel that exists.
    Such a plane could only be created through the API. No decoder produces
    one.

    add_channel() now requires 8 bits for the interleaved RGB and RGBA
    formats and 9 to 16 bits for the RRGGBB formats. The values 24 and 32 are
    still accepted for RGB and RGBA and mean 8, as before. With that, the
    widest pixels are a 128-bit complex sample and four interleaved 16-bit
    components, and the assertion cannot trigger anymore. The function also
    returns the value in its full 16 bits instead of casting it to uint8_t.

    Document the allowed bit depths for heif_image_add_plane().

diff --git a/libheif/api/libheif/heif_image.h b/libheif/api/libheif/heif_image.h
index fe7abc50..30d6822b 100644
--- a/libheif/api/libheif/heif_image.h
+++ b/libheif/api/libheif/heif_image.h
@@ -368,6 +368,11 @@ heif_error heif_image_create(int width, int height,
  * with an interleaved format like RRGGBB where each color is represented by 10 bits,
  * the {@code bit_depth} would be {@code 10} rather than {@code 30}.
  *
+ * <p>The interleaved formats define the bit depth of their components:
+ * {@code heif_chroma_interleaved_RGB} and {@code heif_chroma_interleaved_RGBA} have 8 bits,
+ * the {@code heif_chroma_interleaved_RRGGBB...} formats have 9 to 16 bits.
+ * Other bit depths are rejected for these formats.
+ *
  * <p>For backward compatibility, one can also specify 24bits for RGB and 32bits for RGBA,
  * instead of the preferred 8 bits. However, this use is deprecated.
  *
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index 9ad4c7ce..d45ffb8b 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -418,15 +418,25 @@ Error HeifPixelImage::add_channel(heif_channel channel, uint32_t width, uint32_t

   // The RRGGBB(AA) interleaved formats store each component as 16 bit. A bit depth
   // of <= 8 would be self-inconsistent: the allocated plane would only hold one byte
-  // per component while readers/writers access the samples as 16-bit values.
+  // per component while readers/writers access the samples as 16-bit values. A bit depth
+  // above 16 is no RRGGBB(AA) format either: the components would not be 16-bit words.
   if ((m_chroma == heif_chroma_interleaved_RRGGBB_BE ||
        m_chroma == heif_chroma_interleaved_RRGGBB_LE ||
        m_chroma == heif_chroma_interleaved_RRGGBBAA_BE ||
        m_chroma == heif_chroma_interleaved_RRGGBBAA_LE) &&
-      bit_depth <= 8) {
+      (bit_depth <= 8 || bit_depth > 16)) {
     return {heif_error_Usage_error,
-            heif_suberror_Unspecified,
-            "Cannot create a 16-bit interleaved channel with a bit depth of 8 or less"};
+            heif_suberror_Invalid_parameter_value,
+            "The interleaved RRGGBB formats require a bit depth of 9 to 16"};
+  }
+
+  // The interleaved RGB and RGBA formats have 8 bits per component.
+  if ((m_chroma == heif_chroma_interleaved_RGB ||
+       m_chroma == heif_chroma_interleaved_RGBA) &&
+      bit_depth != 8) {
+    return {heif_error_Usage_error,
+            heif_suberror_Invalid_parameter_value,
+            "The interleaved RGB and RGBA formats require a bit depth of 8"};
   }

   int num_interleaved_pixels = num_interleaved_components_per_plane(m_chroma);
@@ -1198,9 +1208,11 @@ uint16_t HeifPixelImage::get_storage_bits_per_pixel(enum heif_channel channel) c
     return 0;
   }

+  // The widest pixels are a 128-bit complex sample and four interleaved 16-bit components.
+  // add_channel() does not let an interleaved plane have wider components.
   uint32_t bpp = comp->get_bytes_per_pixel() * 8;
   assert(bpp <= 256);
-  return static_cast<uint8_t>(bpp);
+  return static_cast<uint16_t>(bpp);
 }


diff --git a/tests/add_channel_checks.cc b/tests/add_channel_checks.cc
index c38ce4b6..085b4b95 100644
--- a/tests/add_channel_checks.cc
+++ b/tests/add_channel_checks.cc
@@ -112,3 +112,84 @@ TEST_CASE("add_channel does not constrain non-chroma auxiliary planes") {

   REQUIRE(image->add_channel(heif_channel_depth, 5, 5, 8, limits).error_code == heif_error_Ok);
 }
+
+// The interleaved chroma formats define the bit depth of their components: 8 bits for RGB
+// and RGBA, 9 to 16 bits (stored as 16-bit words) for the RRGGBB formats. add_channel()
+// accepted any bit depth for them. An interleaved plane with 64-bit or 128-bit components
+// has pixels of more than 255 bits, for which get_storage_bits_per_pixel() ran into its
+// assertion, or, without assertions, reported a truncated size (384 bits as 128, and 256
+// bits as 0, which the API turned into "no such channel").
+TEST_CASE("add_channel checks the bit depth of interleaved formats") {
+  auto* limits = heif_get_global_security_limits();
+
+  auto add = [limits](heif_chroma chroma, int bit_depth, std::shared_ptr<HeifPixelImage>* out_image = nullptr) {
+    auto image = std::make_shared<HeifPixelImage>();
+    image->create(16, 16, heif_colorspace_RGB, chroma);
+    Error err = image->add_channel(heif_channel_interleaved, 16, 16, bit_depth, limits);
+    if (out_image) {
+      *out_image = image;
+    }
+    return err;
+  };
+
+  SECTION("RGB and RGBA have 8 bits per component") {
+    for (heif_chroma chroma : {heif_chroma_interleaved_RGB, heif_chroma_interleaved_RGBA}) {
+      const int num_components = (chroma == heif_chroma_interleaved_RGB) ? 3 : 4;
+      INFO("chroma " << chroma);
+
+      std::shared_ptr<HeifPixelImage> image;
+      REQUIRE(!add(chroma, 8, &image));
+      CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == 8);
+      CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 8 * num_components);
+
+      // for backwards compatibility, the size of the whole pixel is accepted as well
+      REQUIRE(!add(chroma, 8 * num_components, &image));
+      CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == 8);
+      CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 8 * num_components);
+
+      for (int bit_depth : {1, 7, 9, 10, 16, 64, 128}) {
+        INFO("bit depth " << bit_depth);
+        Error err = add(chroma, bit_depth);
+        CHECK(err.error_code == heif_error_Usage_error);
+        CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+      }
+    }
+  }
+
+  SECTION("the RRGGBB formats have 9 to 16 bits per component") {
+    for (heif_chroma chroma : {heif_chroma_interleaved_RRGGBB_LE, heif_chroma_interleaved_RRGGBB_BE,
+                               heif_chroma_interleaved_RRGGBBAA_LE, heif_chroma_interleaved_RRGGBBAA_BE}) {
+      const int num_components = (chroma == heif_chroma_interleaved_RRGGBB_LE ||
+                                  chroma == heif_chroma_interleaved_RRGGBB_BE) ? 3 : 4;
+      INFO("chroma " << chroma);
+
+      for (int bit_depth : {9, 10, 12, 16}) {
+        INFO("bit depth " << bit_depth);
+        std::shared_ptr<HeifPixelImage> image;
+        REQUIRE(!add(chroma, bit_depth, &image));
+        CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == bit_depth);
+        CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 16 * num_components);
+      }
+
+      for (int bit_depth : {1, 8, 17, 32, 64, 128}) {
+        INFO("bit depth " << bit_depth);
+        Error err = add(chroma, bit_depth);
+        CHECK(err.error_code == heif_error_Usage_error);
+        CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+      }
+    }
+  }
+
+  SECTION("planes of other formats keep their wide components") {
+    auto image = std::make_shared<HeifPixelImage>();
+    image->create(16, 16, heif_colorspace_custom, heif_chroma_planar);
+    REQUIRE(!image->add_channel(heif_channel_Y, 16, 16, 128, limits, heif_component_datatype_complex_number));
+    CHECK(image->get_storage_bits_per_pixel(heif_channel_Y) == 128);
+  }
+
+  SECTION("a channel that does not exist") {
+    auto image = std::make_shared<HeifPixelImage>();
+    image->create(16, 16, heif_colorspace_RGB, heif_chroma_interleaved_RGB);
+    CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 0);
+  }
+}