Commit ebe3148d for libheif

commit ebe3148d5d1d931db93168fffb2d6efc9cee59fe
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Oct 5 04:32:36 2026 +0200

    Check the plane layout and compose 16-bit samples in HeifPixelImage::overlay()

    overlay() reads all planes of the overlay image with the coordinates of
    its color planes. It only compared the alpha plane with the size of the
    image. A HeifPixelImage can have planes of any size, so with color planes
    that are larger than the image the alpha plane passed that check and the
    blend loop read past its end. This needs an image that is built through
    the internal classes: images from a file come from a decoder that creates
    all planes in the image size, or through the color conversion, which
    checks the plane layout. overlay() now calls check_plane_layout() on the
    overlay image itself.

    The planes were composed byte by byte whatever their bit depth, so the
    two bytes of a 16-bit sample were blended like two pixels. They are now
    composed with their sample size:
    - unsigned samples of up to 16 bits are supported,
    - the alpha plane may have another bit depth than the color planes,
    - an alpha sample above the range of its bit depth counts as opaque,
      since the weight of the canvas would wrap around otherwise.

    What cannot be composed is an error: planes with more than 16 bits,
    samples that are not unsigned integers, and an overlay image with
    another bit depth than the canvas.

    fill_RGB_16bit() fills unsigned planes of up to 16 bits instead of 8-bit
    planes only.

    The canvas of an 'iovl' image still has 8 bits per sample after this
    commit, so an overlay of images with more than 8 bits is refused until
    the next commit gives the canvas the bit depth of the images.

diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index d45ffb8b..6084fbe7 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -1996,17 +1996,12 @@ Error HeifPixelImage::fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_

     ComponentStorage& plane = *comp;

-    if (plane.m_bit_depth != 8) {
+    if (plane.m_bit_depth > 16 || plane.m_datatype != heif_component_datatype_unsigned_integer) {
       return {heif_error_Unsupported_feature,
               heif_suberror_Unspecified,
-              "Can currently only fill images with 8 bits per pixel"};
+              "Can only fill images with unsigned samples of up to 16 bits"};
     }

-    size_t h = plane.m_height;
-
-    size_t stride = plane.stride;
-    auto* data = static_cast<uint8_t*>(plane.mem);
-
     uint16_t val16;
     switch (channel) {
       case heif_channel_R:
@@ -2028,23 +2023,14 @@ Error HeifPixelImage::fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_
         assert(false);
     }

-    auto val8 = static_cast<uint8_t>(val16 >> 8U);
-
+    // The values are given with 16 bits. Reduce them to the bit depth of the plane.
+    auto value = static_cast<uint16_t>(val16 >> (16 - plane.m_bit_depth));

-    // memset() even when h * stride > sizeof(size_t)
-
-    if (std::numeric_limits<size_t>::max() / stride > h) {
-      // can fill in one step
-      memset(data, val8, stride * h);
+    if (plane.m_bit_depth <= 8) {
+      plane.fill<uint8_t>(static_cast<uint8_t>(value));
     }
     else {
-      // fill line by line
-      auto* p = data;
-
-      for (size_t y=0;y<h;y++) {
-        memset(p, val8, stride);
-        p += stride;
-      }
+      plane.fill<uint16_t>(value);
     }
   }

@@ -2052,6 +2038,21 @@ Error HeifPixelImage::fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_
 }


+// Blends one row of the overlay into the canvas: out = in * alpha + out * (1 - alpha).
+template <typename T, typename A>
+static void blend_row(T* out, const T* in, const A* alpha, uint32_t n, uint32_t alpha_max)
+{
+  for (uint32_t x = 0; x < n; x++) {
+    // An alpha sample above its bit depth would make (alpha_max - a) wrap around.
+    uint32_t a = std::min<uint32_t>(alpha[x], alpha_max);
+
+    // in and out are at most 65535 and the two weights add up to alpha_max (at most 65535),
+    // so the sum fits into 32 bits.
+    out[x] = static_cast<T>((uint32_t{in[x]} * a + uint32_t{out[x]} * (alpha_max - a)) / alpha_max);
+  }
+}
+
+
 Error HeifPixelImage::overlay(std::shared_ptr<HeifPixelImage>& overlay, int32_t dx, int32_t dy)
 {
   // This function places the overlay using the full-resolution (dx,dy) offset
@@ -2077,19 +2078,48 @@ Error HeifPixelImage::overlay(std::shared_ptr<HeifPixelImage>& overlay, int32_t

   // The blend loop below indexes the Alpha plane using the extent of each color
   // channel (in_w/in_h, out_w/out_h), not the Alpha plane's own reported extent.
-  // If the Alpha plane were smaller than the other channels, that would read past
-  // its allocation, so reject that case up front instead of trusting the sizes
-  // to agree.
+  // All planes of the overlay therefore have to have the same size. A HeifPixelImage
+  // does not guarantee that: its planes can have any size. Comparing only the Alpha
+  // plane with the size of the image was not enough, since the color planes can be
+  // larger than the image, and the loop then read past the end of the Alpha plane.
+  // check_plane_layout() checks all planes against the size of the image.
   // Note that differently sized Alpha channels are allowed, but we currently do
   // not support it here (TODO).
-  if (has_alpha &&
-      (overlay->get_width(heif_channel_Alpha) != overlay->get_width() ||
-       overlay->get_height(heif_channel_Alpha) != overlay->get_height())) {
+  if (Error err = overlay->check_plane_layout()) {
     return {heif_error_Unsupported_feature,
             heif_suberror_Unspecified,
-            "Overlay image Alpha plane size does not match the other color planes"};
+            "Cannot overlay image: " + err.message};
+  }
+
+  // The planes are composed sample by sample, with 8-bit or 16-bit samples. A plane of the
+  // overlay and the plane of the canvas it is drawn into have to have the same bit depth.
+  // (They used to be composed byte by byte whatever their bit depth, so that the two bytes
+  // of a 16-bit sample were blended as if they were two pixels.)
+  for (heif_channel channel : channels) {
+    const bool used = (channel == heif_channel_Alpha) || has_channel(channel);
+    if (!used) {
+      continue;
+    }
+
+    if (overlay->get_bits_per_pixel(channel) > 16 ||
+        overlay->get_datatype(channel) != heif_component_datatype_unsigned_integer) {
+      return {heif_error_Unsupported_feature,
+              heif_suberror_Unspecified,
+              "Overlaying images is only implemented for unsigned samples of up to 16 bits"};
+    }
+
+    if (has_channel(channel) &&
+        (get_bits_per_pixel(channel) != overlay->get_bits_per_pixel(channel) ||
+         get_datatype(channel) != heif_component_datatype_unsigned_integer)) {
+      return {heif_error_Unsupported_feature,
+              heif_suberror_Unspecified,
+              "Overlaying an image onto an image with another bit depth is not implemented"};
+    }
   }

+  const int alpha_bytes = has_alpha ? bytes_per_sample_for_bit_depth(overlay->get_bits_per_pixel(heif_channel_Alpha)) : 0;
+  const uint32_t alpha_max = has_alpha ? ((1U << overlay->get_bits_per_pixel(heif_channel_Alpha)) - 1) : 0;
+
   size_t alpha_stride = 0;
   uint8_t* alpha_p;
   alpha_p = overlay->get_channel_memory(heif_channel_Alpha, &alpha_stride);
@@ -2149,19 +2179,32 @@ Error HeifPixelImage::overlay(std::shared_ptr<HeifPixelImage>& overlay, int32_t

     // --- composite the overlay in the overlapping area

+    const int bytes = bytes_per_sample_for_bit_depth(overlay->get_bits_per_pixel(channel)); // 1 or 2, checked above
+
     for (uint32_t y = 0; y < copy_h; y++) {
-      const uint8_t* in_row = in_p + in_x0 + static_cast<size_t>(in_y0 + y) * in_stride;
-      uint8_t* out_row = out_p + out_x0 + static_cast<size_t>(out_y0 + y) * out_stride;
+      const uint8_t* in_row = in_p + static_cast<size_t>(in_x0) * bytes + static_cast<size_t>(in_y0 + y) * in_stride;
+      uint8_t* out_row = out_p + static_cast<size_t>(out_x0) * bytes + static_cast<size_t>(out_y0 + y) * out_stride;

       if (!has_alpha) {
-        memcpy(out_row, in_row, copy_w);
+        memcpy(out_row, in_row, static_cast<size_t>(copy_w) * bytes);
+        continue;
       }
-      else {
-        const uint8_t* alpha_row = alpha_p + in_x0 + static_cast<size_t>(in_y0 + y) * alpha_stride;

-        for (uint32_t x = 0; x < copy_w; x++) {
-          out_row[x] = static_cast<uint8_t>((in_row[x] * alpha_row[x] + out_row[x] * (255 - alpha_row[x])) / 255);
-        }
+      const uint8_t* alpha_row = alpha_p + static_cast<size_t>(in_x0) * alpha_bytes + static_cast<size_t>(in_y0 + y) * alpha_stride;
+
+      if (bytes == 1 && alpha_bytes == 1) {
+        blend_row(out_row, in_row, alpha_row, copy_w, alpha_max);
+      }
+      else if (bytes == 1) {
+        blend_row(out_row, in_row, reinterpret_cast<const uint16_t*>(alpha_row), copy_w, alpha_max);
+      }
+      else if (alpha_bytes == 1) {
+        blend_row(reinterpret_cast<uint16_t*>(out_row), reinterpret_cast<const uint16_t*>(in_row),
+                  alpha_row, copy_w, alpha_max);
+      }
+      else {
+        blend_row(reinterpret_cast<uint16_t*>(out_row), reinterpret_cast<const uint16_t*>(in_row),
+                  reinterpret_cast<const uint16_t*>(alpha_row), copy_w, alpha_max);
       }
     }
   }
diff --git a/tests/pixelimage_overlay.cc b/tests/pixelimage_overlay.cc
index 5f73cea4..83f2048d 100644
--- a/tests/pixelimage_overlay.cc
+++ b/tests/pixelimage_overlay.cc
@@ -30,6 +30,7 @@
 #include <climits>
 #include <cstring>
 #include <memory>
+#include <utility>
 #include <vector>

 // Regression tests for HeifPixelImage::overlay() with the overlay image placed
@@ -221,3 +222,372 @@ TEST_CASE("overlay completely outside the canvas leaves it unchanged") {
     check_overlay_at(o.dx, o.dy, true);
   }
 }
+
+
+// overlay() reads all planes of the overlay with the coordinates of the color planes and
+// blends them byte by byte. HeifPixelImage itself puts no constraint on the size or the
+// bit depth of R, G, B and alpha planes, so overlay() has to refuse an image that does not
+// have the form it needs. It only compared the alpha plane with the size of the image:
+// with color planes that are larger than the image, the alpha plane passed that check and
+// the blend loop read past its end.
+
+namespace {
+
+// An overlay whose planes can have sizes and bit depths that do not fit the image.
+std::shared_ptr<HeifPixelImage> make_irregular_overlay(uint32_t image_size,
+                                                       uint32_t color_plane_size, int color_bits,
+                                                       uint32_t alpha_plane_size, int alpha_bits)
+{
+  auto* limits = heif_get_global_security_limits();
+
+  auto img = std::make_shared<HeifPixelImage>();
+  img->create(image_size, image_size, heif_colorspace_RGB, heif_chroma_444);
+
+  for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    REQUIRE(img->fill_new_channel(ch, 100, color_plane_size, color_plane_size, color_bits, limits).error_code == heif_error_Ok);
+  }
+
+  if (alpha_plane_size > 0) {
+    REQUIRE(img->fill_new_channel(heif_channel_Alpha, 100, alpha_plane_size, alpha_plane_size, alpha_bits, limits).error_code == heif_error_Ok);
+  }
+
+  return img;
+}
+
+void require_canvas_unchanged(const std::shared_ptr<HeifPixelImage>& canvas)
+{
+  for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    size_t stride = 0;
+    const uint8_t* p = canvas->get_channel_memory(ch, &stride);
+    for (uint32_t y = 0; y < CANVAS; y++) {
+      for (uint32_t x = 0; x < CANVAS; x++) {
+        REQUIRE(p[y * stride + x] == BACKGROUND);
+      }
+    }
+  }
+}
+
+} // namespace
+
+
+TEST_CASE("overlay refuses an image whose planes do not have the size of the image") {
+  auto canvas = make_canvas();
+
+  SECTION("control: regular overlay with alpha") {
+    auto overlay = make_irregular_overlay(4, 4, 8, 4, 8);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Ok);
+  }
+
+  SECTION("color planes larger than the image, alpha plane of the image size") {
+    // 64x64 color planes on a 4x4 image: the blend loop used to read 64 rows and columns of
+    // an alpha plane that has 4.
+    auto overlay = make_irregular_overlay(4, 64, 8, 4, 8);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_canvas_unchanged(canvas);
+  }
+
+  SECTION("color planes larger than the image, no alpha plane") {
+    auto overlay = make_irregular_overlay(4, 64, 8, 0, 8);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_canvas_unchanged(canvas);
+  }
+
+  SECTION("alpha plane smaller than the image") {
+    auto overlay = make_irregular_overlay(4, 4, 8, 2, 8);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_canvas_unchanged(canvas);
+  }
+
+  SECTION("alpha plane larger than the image") {
+    auto overlay = make_irregular_overlay(4, 4, 8, 64, 8);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_canvas_unchanged(canvas);
+  }
+}
+
+
+// --- samples with more than 8 bits
+//
+// overlay() composed the planes byte by byte whatever their bit depth. The two bytes of a
+// 16-bit sample were blended like two pixels, and only the left half of each row was
+// drawn. The samples are now composed with their own width (8 or 16 bits), the alpha plane
+// may have another bit depth than the color planes, and what cannot be composed (planes
+// with more than 16 bits, samples that are not unsigned integers, an overlay with another
+// bit depth than the canvas) is refused.
+
+namespace {
+
+uint32_t max_sample(int bits)
+{
+  return (uint32_t{1} << bits) - 1;
+}
+
+// The values use the whole range of the bit depth, with different upper and lower bytes.
+uint16_t wide_background(int bits)
+{
+  return static_cast<uint16_t>((BACKGROUND * 257 + 77) >> (16 - bits));
+}
+
+uint16_t wide_alpha_value(uint32_t x, uint32_t y, int bits)
+{
+  switch ((x + y) % 3) {
+    case 0: return 0;
+    case 1: return static_cast<uint16_t>(max_sample(bits));
+    default: return static_cast<uint16_t>(max_sample(bits) / 3);
+  }
+}
+
+uint16_t wide_overlay_value(heif_channel ch, uint32_t x, uint32_t y, int bits)
+{
+  if (ch == heif_channel_Alpha) {
+    return wide_alpha_value(x, y, bits);
+  }
+
+  return static_cast<uint16_t>(((overlay_value(ch, x, y) * 257) ^ 0x5A) >> (16 - bits));
+}
+
+void write_sample(const std::shared_ptr<HeifPixelImage>& img, heif_channel ch, uint32_t x, uint32_t y, uint16_t value)
+{
+  size_t stride = 0;
+  uint8_t* p = img->get_channel_memory(ch, &stride);
+
+  if (img->get_bits_per_pixel(ch) <= 8) {
+    p[y * stride + x] = static_cast<uint8_t>(value);
+  }
+  else {
+    memcpy(p + y * stride + 2 * x, &value, 2);
+  }
+}
+
+uint16_t read_sample(const std::shared_ptr<HeifPixelImage>& img, heif_channel ch, uint32_t x, uint32_t y)
+{
+  size_t stride = 0;
+  const uint8_t* p = img->get_channel_memory(ch, &stride);
+
+  if (img->get_bits_per_pixel(ch) <= 8) {
+    return p[y * stride + x];
+  }
+
+  uint16_t value;
+  memcpy(&value, p + y * stride + 2 * x, 2);
+  return value;
+}
+
+std::shared_ptr<HeifPixelImage> make_wide_canvas(int bits,
+                                                 heif_component_datatype datatype = heif_component_datatype_unsigned_integer)
+{
+  auto img = std::make_shared<HeifPixelImage>();
+  img->create(CANVAS, CANVAS, heif_colorspace_RGB, heif_chroma_444);
+
+  for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    REQUIRE(img->add_channel(ch, CANVAS, CANVAS, bits, heif_get_global_security_limits(), datatype).error_code == heif_error_Ok);
+
+    if (bits <= 16) {
+      for (uint32_t y = 0; y < CANVAS; y++) {
+        for (uint32_t x = 0; x < CANVAS; x++) {
+          write_sample(img, ch, x, y, wide_background(bits));
+        }
+      }
+    }
+  }
+
+  return img;
+}
+
+// 'alpha_bits' = 0: no alpha plane.
+std::shared_ptr<HeifPixelImage> make_wide_overlay(int color_bits, int alpha_bits,
+                                                  heif_component_datatype color_datatype = heif_component_datatype_unsigned_integer,
+                                                  heif_component_datatype alpha_datatype = heif_component_datatype_unsigned_integer)
+{
+  auto img = std::make_shared<HeifPixelImage>();
+  img->create(OVL_W, OVL_H, heif_colorspace_RGB, heif_chroma_444);
+
+  std::vector<heif_channel> channels = {heif_channel_R, heif_channel_G, heif_channel_B};
+  if (alpha_bits > 0) {
+    channels.push_back(heif_channel_Alpha);
+  }
+
+  for (heif_channel ch : channels) {
+    const bool is_alpha = (ch == heif_channel_Alpha);
+    const int bits = is_alpha ? alpha_bits : color_bits;
+
+    REQUIRE(img->add_channel(ch, OVL_W, OVL_H, bits, heif_get_global_security_limits(),
+                             is_alpha ? alpha_datatype : color_datatype).error_code == heif_error_Ok);
+
+    if (bits <= 16) {
+      for (uint32_t y = 0; y < OVL_H; y++) {
+        for (uint32_t x = 0; x < OVL_W; x++) {
+          write_sample(img, ch, x, y, wide_overlay_value(ch, x, y, bits));
+        }
+      }
+    }
+  }
+
+  return img;
+}
+
+void require_wide_canvas_unchanged(const std::shared_ptr<HeifPixelImage>& canvas, int bits)
+{
+  for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    for (uint32_t y = 0; y < CANVAS; y++) {
+      for (uint32_t x = 0; x < CANVAS; x++) {
+        REQUIRE(read_sample(canvas, ch, x, y) == wide_background(bits));
+      }
+    }
+  }
+}
+
+void check_wide_overlay_at(int32_t dx, int32_t dy, int color_bits, int alpha_bits)
+{
+  INFO("offset (" << dx << "," << dy << "), " << color_bits << "-bit color, " << alpha_bits << "-bit alpha");
+
+  auto canvas = make_wide_canvas(color_bits);
+  auto overlay = make_wide_overlay(color_bits, alpha_bits);
+
+  Error err = canvas->overlay(overlay, dx, dy);
+  INFO("error: " << err.message);
+  REQUIRE(err.error_code == heif_error_Ok);
+
+  for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+    REQUIRE(canvas->get_bits_per_pixel(ch) == color_bits);
+
+    for (uint32_t cy = 0; cy < CANVAS; cy++) {
+      for (uint32_t cx = 0; cx < CANVAS; cx++) {
+        INFO("channel " << static_cast<int>(ch) << ", canvas pixel (" << cx << "," << cy << ")");
+
+        int64_t ox = static_cast<int64_t>(cx) - dx;
+        int64_t oy = static_cast<int64_t>(cy) - dy;
+
+        uint64_t expected = wide_background(color_bits);
+        if (ox >= 0 && oy >= 0 && ox < OVL_W && oy < OVL_H) {
+          uint64_t in = wide_overlay_value(ch, static_cast<uint32_t>(ox), static_cast<uint32_t>(oy), color_bits);
+
+          if (alpha_bits == 0) {
+            expected = in;
+          }
+          else {
+            uint64_t a = wide_alpha_value(static_cast<uint32_t>(ox), static_cast<uint32_t>(oy), alpha_bits);
+            uint64_t a_max = max_sample(alpha_bits);
+            expected = (in * a + expected * (a_max - a)) / a_max;
+          }
+        }
+
+        REQUIRE(read_sample(canvas, ch, cx, cy) == expected);
+      }
+    }
+  }
+}
+
+} // namespace
+
+
+TEST_CASE("overlay of images with more than 8 bits per sample") {
+  // {color bits, alpha bits}; 0 = no alpha plane
+  const std::vector<std::pair<int, int>> formats = {
+      {10, 0}, {12, 0}, {16, 0},     // no alpha
+      {10, 10}, {12, 12}, {16, 16},  // alpha with the bit depth of the color planes
+      {10, 8}, {16, 8},              // 8-bit alpha on wide color planes
+      {8, 16}, {8, 10},              // wide alpha on 8-bit color planes
+      {12, 16}, {16, 9},             // different wide bit depths
+      {8, 1}, {16, 1},               // binary alpha
+  };
+
+  for (const auto& format : formats) {
+    for (const auto& o : partially_visible_offsets) {
+      check_wide_overlay_at(o.dx, o.dy, format.first, format.second);
+    }
+
+    for (const auto& o : invisible_offsets) {
+      check_wide_overlay_at(o.dx, o.dy, format.first, format.second);
+    }
+  }
+}
+
+
+TEST_CASE("overlay limits alpha samples to the range of their bit depth") {
+  // Nothing guarantees that the samples of a 10-bit plane are below 1024. With an alpha
+  // sample above the maximum, the weight of the canvas (maximum - alpha) would wrap around.
+  for (int color_bits : {8, 16}) {
+    auto canvas = make_wide_canvas(color_bits);
+    auto overlay = make_wide_overlay(color_bits, 10);
+
+    for (uint32_t y = 0; y < OVL_H; y++) {
+      for (uint32_t x = 0; x < OVL_W; x++) {
+        write_sample(overlay, heif_channel_Alpha, x, y, 0xFFFF);
+      }
+    }
+
+    REQUIRE(canvas->overlay(overlay, 0, 0).error_code == heif_error_Ok);
+
+    // treated as an opaque pixel
+    for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) {
+      for (uint32_t y = 0; y < OVL_H; y++) {
+        for (uint32_t x = 0; x < OVL_W; x++) {
+          REQUIRE(read_sample(canvas, ch, x, y) == wide_overlay_value(ch, x, y, color_bits));
+        }
+      }
+    }
+  }
+}
+
+
+TEST_CASE("overlay refuses sample formats that it cannot compose") {
+  SECTION("overlay with more bits than the canvas") {
+    for (int bits : {10, 16}) {
+      auto canvas = make_wide_canvas(8);
+      auto overlay = make_wide_overlay(bits, 0);
+      CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+      require_wide_canvas_unchanged(canvas, 8);
+    }
+  }
+
+  SECTION("overlay with fewer bits than the canvas") {
+    for (int alpha_bits : {0, 8}) {
+      auto canvas = make_wide_canvas(16);
+      auto overlay = make_wide_overlay(8, alpha_bits);
+      CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+      require_wide_canvas_unchanged(canvas, 16);
+    }
+  }
+
+  SECTION("different bit depths with the same sample size") {
+    auto canvas = make_wide_canvas(12);
+    auto overlay = make_wide_overlay(10, 0);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_wide_canvas_unchanged(canvas, 12);
+  }
+
+  SECTION("color planes with more than 16 bits") {
+    auto canvas = make_wide_canvas(32);
+    auto overlay = make_wide_overlay(32, 0);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+  }
+
+  SECTION("alpha plane with more than 16 bits") {
+    auto canvas = make_wide_canvas(8);
+    auto overlay = make_wide_overlay(8, 32);
+    CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    require_wide_canvas_unchanged(canvas, 8);
+  }
+
+  SECTION("samples that are not unsigned integers") {
+    {
+      auto canvas = make_wide_canvas(16, heif_component_datatype_signed_integer);
+      auto overlay = make_wide_overlay(16, 0, heif_component_datatype_signed_integer);
+      CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    }
+
+    {
+      auto canvas = make_wide_canvas(16);
+      auto overlay = make_wide_overlay(16, 16, heif_component_datatype_unsigned_integer, heif_component_datatype_signed_integer);
+      CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+      require_wide_canvas_unchanged(canvas, 16);
+    }
+
+    {
+      auto canvas = make_wide_canvas(16, heif_component_datatype_signed_integer);
+      auto overlay = make_wide_overlay(16, 0);
+      CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature);
+    }
+  }
+}