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);
+ }
+}