Commit e65071f5 for libheif
commit e65071f59a1ac08aa1eb0d07a831deaf6bb4d03b
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Aug 24 20:41:18 2026 +0200
validate tile images against the unci encoder configuration (GHSA-j264-xvrp-5v7q)
The unci encoder is constructed once from the prototype image passed to
heif_context_add_empty_unci_image() and keeps that configuration: the component
ids it addresses the planes by, their bit depths, the uncC subsampling type and
the number of bytes per pixel. heif_context_add_image_tile() then handed
independently built images straight to encode_tile(), which sizes its output
buffer from that frozen configuration while copying from the tile's actual
planes. A tile that did not match wrote past the end of that buffer.
check_component_sizes() was only called from unc_encoder::encode(), so the tile
path had no check at all. Calling it there would not have been enough either: it
compares an image against its own dimensions and chroma format, so a well formed
4:4:4 tile passes while the encoder still assumes the 4:2:0 subsampling of the
prototype and allocates only half the chroma.
Make the check impossible to bypass. encode_tile() is now a non-virtual gate
that validates the image and forwards to the protected encode_tile_impl(). It
compares colorspace and chroma format against the prototype, looks up every
component the encoder will write by id and compares channel, component type, bit
depth and datatype, and finally checks the plane sizes.
check_component_sizes() itself walked get_used_planar_component_ids(), which
does not list interleaved planes, so it was a no-op for interleaved images on
the already guarded encode() path as well. It now iterates the component
descriptions and skips cpat reference components.
Also reject tiles whose size differs from the tile size of the image. The write
offset is computed from the size of the passed image, so a differently sized
tile ended up at a wrong offset and was silently truncated by
Box_iloc::replace_data.
Reported by Burak Sakizci.
diff --git a/libheif/codecs/uncompressed/unc_encoder.cc b/libheif/codecs/uncompressed/unc_encoder.cc
index 936075e5..5421066d 100644
--- a/libheif/codecs/uncompressed/unc_encoder.cc
+++ b/libheif/codecs/uncompressed/unc_encoder.cc
@@ -82,6 +82,20 @@ unc_encoder::unc_encoder(const std::shared_ptr<const HeifPixelImage>& image)
m_map_id_to_cmpd_index[ids[i]] = static_cast<uint32_t>(i);
}
+ // --- remember the image configuration we are generating this encoder for
+
+ m_prototype_colorspace = image->get_colorspace();
+ m_prototype_chroma = image->get_chroma_format();
+
+ for (const auto& desc : image->get_component_descriptions()) {
+ m_prototype_components.push_back({desc.component_id,
+ desc.channel,
+ desc.component_type,
+ desc.bit_depth,
+ desc.datatype,
+ desc.has_data_plane});
+ }
+
// TODO: we could combine component_ids with similar types if they are also used in the same way in the metadata boxes
// --- create cmpd component types
@@ -191,13 +205,19 @@ Error unc_encoder::check_component_sizes(const std::shared_ptr<const HeifPixelIm
uint32_t image_height = src_image->get_height();
heif_chroma chroma = src_image->get_chroma_format();
- for (uint32_t id : src_image->get_used_planar_component_ids()) {
- heif_channel channel = src_image->get_component_channel(id);
+ // Iterate the component descriptions rather than get_used_planar_component_ids() so that
+ // components of an interleaved plane are covered too. Reference components of a cpat pattern
+ // carry no pixels and are skipped.
+
+ for (const auto& desc : src_image->get_component_descriptions()) {
+ if (!desc.has_data_plane) {
+ continue;
+ }
uint32_t expected_width = image_width;
uint32_t expected_height = image_height;
- if (channel == heif_channel_Cb || channel == heif_channel_Cr) {
+ if (desc.channel == heif_channel_Cb || desc.channel == heif_channel_Cr) {
if (chroma == heif_chroma_420) {
expected_width = (image_width + 1) / 2;
expected_height = (image_height + 1) / 2;
@@ -207,8 +227,8 @@ Error unc_encoder::check_component_sizes(const std::shared_ptr<const HeifPixelIm
}
}
- if (src_image->get_component_width(id) != expected_width ||
- src_image->get_component_height(id) != expected_height) {
+ if (desc.width != expected_width ||
+ desc.height != expected_height) {
return {heif_error_Invalid_input,
heif_suberror_Unspecified,
"Image component plane size does not match the image dimensions"};
@@ -219,15 +239,79 @@ Error unc_encoder::check_component_sizes(const std::shared_ptr<const HeifPixelIm
}
-Result<Encoder::CodedImageData> unc_encoder::encode(const std::shared_ptr<const HeifPixelImage>& src_image,
- const heif_encoding_options& in_options) const
+static Error incompatible_image_error()
{
+ return {heif_error_Invalid_input,
+ heif_suberror_Unspecified,
+ "Image does not match the component configuration of the uncompressed image it is added to"};
+}
+
+
+Error unc_encoder::check_image_compatibility(const std::shared_ptr<const HeifPixelImage>& image) const
+{
+ // The subsampling factors and the number of bytes per pixel are taken from the prototype image.
+
+ if (image->get_colorspace() != m_prototype_colorspace ||
+ image->get_chroma_format() != m_prototype_chroma) {
+ return incompatible_image_error();
+ }
+
+ // The encoders address the component planes by the component ids of the prototype image and use
+ // its bit depths and data types to compute the output size. All of these have to match.
+ //
+ // We look the components up by id instead of comparing the description lists position by
+ // position, because that is how the encoders access them (find_component_description() searches
+ // by id). The order of the description list is not part of the contract: descriptions are
+ // appended by whoever builds the image and transfer_channel_from_image_as() even erases entries
+ // from the middle of the list, so equal images may well list their components in a different
+ // order.
+
+ if (image->get_component_descriptions().size() != m_prototype_components.size()) {
+ return incompatible_image_error();
+ }
+
+ for (const prototype_component& proto : m_prototype_components) {
+ const ComponentDescription* desc = image->find_component_description(proto.component_id);
+
+ if (desc == nullptr ||
+ desc->channel != proto.channel ||
+ desc->component_type != proto.component_type ||
+ desc->bit_depth != proto.bit_depth ||
+ desc->datatype != proto.datatype ||
+ desc->has_data_plane != proto.has_data_plane) {
+ return incompatible_image_error();
+ }
+ }
+
+ // Every prototype component was found and the lists are equally long, so the tile image has
+ // exactly the components the encoder will write and no extra ones that would be silently
+ // dropped.
+
// The encoders size their output buffer from the primary image dimensions, but copy each
// component plane using that plane's actual size. If a component plane is larger than the primary
// image (e.g. an alpha plane that does not match the color planes), this writes out of bounds.
// Reject any image whose component planes are inconsistent with the primary size. Chroma (Cb/Cr)
// planes are legitimately subsampled, so they are checked against the subsampled size.
- if (Error err = check_component_sizes(src_image)) {
+
+ return check_component_sizes(image);
+}
+
+
+Result<std::vector<uint8_t>> unc_encoder::encode_tile(const std::shared_ptr<const HeifPixelImage>& image) const
+{
+ if (Error err = check_image_compatibility(image)) {
+ return err;
+ }
+
+ return encode_tile_impl(image);
+}
+
+
+Result<Encoder::CodedImageData> unc_encoder::encode(const std::shared_ptr<const HeifPixelImage>& src_image,
+ const heif_encoding_options& in_options) const
+{
+ // Fail before generating any boxes. encode_tile() checks this again.
+ if (Error err = check_image_compatibility(src_image)) {
return err;
}
diff --git a/libheif/codecs/uncompressed/unc_encoder.h b/libheif/codecs/uncompressed/unc_encoder.h
index 5845b77c..cbee181d 100644
--- a/libheif/codecs/uncompressed/unc_encoder.h
+++ b/libheif/codecs/uncompressed/unc_encoder.h
@@ -64,17 +64,33 @@ public:
virtual uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const = 0;
- [[nodiscard]] virtual std::vector<uint8_t> encode_tile(const std::shared_ptr<const HeifPixelImage>& image) const = 0;
+ // Encode one tile. This validates 'image' against the encoder configuration before handing it
+ // to the actual encoder implementation. Do not bypass it by calling encode_tile_impl() directly.
+ [[nodiscard]] Result<std::vector<uint8_t>> encode_tile(const std::shared_ptr<const HeifPixelImage>& image) const;
Result<Encoder::CodedImageData> encode(const std::shared_ptr<const HeifPixelImage>& src_image,
const heif_encoding_options& options) const;
+ // Check that 'image' may be passed to encode_tile().
+ //
+ // An encoder freezes its configuration (component list, bit depths, subsampling, bytes per pixel)
+ // when it is constructed from a prototype image. The tiled writing path
+ // (ImageItem_uncompressed::add_image_tile()) hands us independently constructed images later on,
+ // so every one of them has to be checked against that frozen configuration. Otherwise
+ // encode_tile_impl() sizes its output buffer according to the configuration while copying from
+ // planes with a different layout, which reads and writes outside the buffers.
+ Error check_image_compatibility(const std::shared_ptr<const HeifPixelImage>& image) const;
+
// Verify that every component plane has the size implied by the primary image dimensions
// (chroma planes may be subsampled). The encoders assume this when sizing their output buffer,
// so a mismatched plane would otherwise overflow the buffer during encoding.
static Error check_component_sizes(const std::shared_ptr<const HeifPixelImage>& src_image);
protected:
+ // Encode one tile. Only called through encode_tile(), which has verified that 'image' matches
+ // the configuration this encoder was constructed with.
+ [[nodiscard]] virtual std::vector<uint8_t> encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& image) const = 0;
+
std::shared_ptr<Box_cmpd> m_cmpd;
std::shared_ptr<Box_uncC> m_uncC;
std::shared_ptr<Box_cpat> m_cpat;
@@ -84,6 +100,23 @@ protected:
std::shared_ptr<Box_cloc> m_cloc;
std::map<uint32_t, uint32_t> m_map_id_to_cmpd_index;
+
+private:
+ // Configuration of the prototype image this encoder was constructed for. Tile images passed to
+ // encode_tile() have to match it. See check_image_compatibility().
+ struct prototype_component
+ {
+ uint32_t component_id;
+ heif_channel channel;
+ uint16_t component_type;
+ uint16_t bit_depth;
+ heif_component_datatype datatype;
+ bool has_data_plane;
+ };
+
+ heif_colorspace m_prototype_colorspace = heif_colorspace_undefined;
+ heif_chroma m_prototype_chroma = heif_chroma_undefined;
+ std::vector<prototype_component> m_prototype_components;
};
diff --git a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
index 4c88d71d..636483d5 100644
--- a/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc
@@ -187,7 +187,7 @@ uint64_t unc_encoder_component_interleave::compute_tile_data_size_bytes(uint32_t
}
-std::vector<uint8_t> unc_encoder_component_interleave::encode_tile(const std::shared_ptr<const HeifPixelImage>& src_image) const
+std::vector<uint8_t> unc_encoder_component_interleave::encode_tile_impl(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;
diff --git a/libheif/codecs/uncompressed/unc_encoder_component_interleave.h b/libheif/codecs/uncompressed/unc_encoder_component_interleave.h
index e7d7aa3c..cbe734ca 100644
--- a/libheif/codecs/uncompressed/unc_encoder_component_interleave.h
+++ b/libheif/codecs/uncompressed/unc_encoder_component_interleave.h
@@ -35,7 +35,7 @@ public:
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;
+ [[nodiscard]] std::vector<uint8_t> encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& image) const override;
private:
struct channel_component
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc
index 2b8e0d86..82aa8ada 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc
@@ -86,7 +86,7 @@ uint64_t unc_encoder_rgb_block_pixel_interleave::compute_tile_data_size_bytes(ui
}
-std::vector<uint8_t> unc_encoder_rgb_block_pixel_interleave::encode_tile(const std::shared_ptr<const HeifPixelImage>& src_image) const
+std::vector<uint8_t> unc_encoder_rgb_block_pixel_interleave::encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& src_image) const
{
std::vector<uint8_t> data;
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h b/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h
index 170a5554..25611a68 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h
@@ -36,7 +36,7 @@ public:
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;
+ [[nodiscard]] std::vector<uint8_t> encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& image) const override;
private:
uint8_t m_bytes_per_pixel = 0;
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc
index c359d610..756344c6 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc
@@ -113,7 +113,7 @@ void *memcpy_swap16(uint8_t *dst, const uint8_t *src, size_t n)
}
-std::vector<uint8_t> unc_encoder_rgb_bytealign_pixel_interleave::encode_tile(const std::shared_ptr<const HeifPixelImage>& src_image) const
+std::vector<uint8_t> unc_encoder_rgb_bytealign_pixel_interleave::encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& src_image) const
{
std::vector<uint8_t> data;
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h b/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h
index 7ada28fd..e20c5f22 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h
@@ -34,7 +34,7 @@ public:
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;
+ [[nodiscard]] std::vector<uint8_t> encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& image) const override;
private:
uint8_t m_bytes_per_pixel = 0;
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc b/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc
index 36a0e3c0..26b4cc7c 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc
@@ -95,7 +95,7 @@ uint64_t unc_encoder_rgb_pixel_interleave::compute_tile_data_size_bytes(uint32_t
}
-std::vector<uint8_t> unc_encoder_rgb_pixel_interleave::encode_tile(const std::shared_ptr<const HeifPixelImage>& src_image) const
+std::vector<uint8_t> unc_encoder_rgb_pixel_interleave::encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& src_image) const
{
std::vector<uint8_t> data;
diff --git a/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h b/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h
index e96ba4af..e51f17ad 100644
--- a/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h
+++ b/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h
@@ -34,7 +34,7 @@ public:
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;
+ [[nodiscard]] std::vector<uint8_t> encode_tile_impl(const std::shared_ptr<const HeifPixelImage>& image) const override;
private:
uint8_t m_bytes_per_pixel = 0;
diff --git a/libheif/image-items/unc_image.cc b/libheif/image-items/unc_image.cc
index c866f2f4..ad85cb70 100644
--- a/libheif/image-items/unc_image.cc
+++ b/libheif/image-items/unc_image.cc
@@ -326,6 +326,20 @@ Error ImageItem_uncompressed::add_image_tile(uint32_t tile_x, uint32_t tile_y, c
"tile_x and/or tile_y are out of range."};
}
+ // All tiles have the same size (add_unci_item() enforces that the image size is an integer
+ // multiple of the tile size). We compute the position at which the tile data is written from the
+ // size of the passed image, so a differently sized tile would be written to a wrong offset.
+
+ uint32_t expected_tile_width, expected_tile_height;
+ get_tile_size(expected_tile_width, expected_tile_height);
+
+ if (tile_width != expected_tile_width ||
+ tile_height != expected_tile_height) {
+ return Error{heif_error_Usage_error,
+ heif_suberror_Invalid_parameter_value,
+ "Tile image size does not match the tile size of the uncompressed image."};
+ }
+
if (image->has_alpha() && !save_alpha) {
// TODO: drop alpha
diff --git a/tests/uncompressed_encode.cc b/tests/uncompressed_encode.cc
index 8e4b2873..4161e0fc 100644
--- a/tests/uncompressed_encode.cc
+++ b/tests/uncompressed_encode.cc
@@ -1065,3 +1065,173 @@ TEST_CASE("Encode rejects oversized component plane")
heif_image_release(image);
heif_context_free(ctx);
}
+
+
+// Helper for the tile-add tests below: builds a YCbCr image with individually
+// chosen chroma plane sizes.
+static heif_image* createImage_YCbCr_customPlanes(int w, int h, heif_chroma chroma,
+ int cb_w, int cb_h, int cr_w, int cr_h)
+{
+ heif_image *image;
+ heif_error err = heif_image_create(w, h, heif_colorspace_YCbCr, chroma, &image);
+ REQUIRE(err.code == heif_error_Ok);
+
+ err = heif_image_add_plane(image, heif_channel_Y, w, h, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(image, heif_channel_Cb, cb_w, cb_h, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(image, heif_channel_Cr, cr_w, cr_h, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ return image;
+}
+
+
+// heif_context_add_image_tile() hands the tile image directly to the encoder that was
+// built from the prototype image passed to heif_context_add_empty_unci_image(). The
+// encoder sizes its output buffer from its own configuration while copying from the
+// tile's planes, so a tile that does not match that configuration used to write past
+// the end of that buffer.
+TEST_CASE("Add tile rejects images that do not match the unci configuration")
+{
+ const int TW = 16;
+ const int TH = 16;
+
+ auto add_tile = [](heif_image *prototype, heif_image *tile) -> heif_error {
+ heif_unci_image_parameters params{};
+ params.version = 1;
+ params.image_width = 2 * TW;
+ params.image_height = 2 * TH;
+ params.tile_width = TW;
+ params.tile_height = TH;
+ params.compression = heif_unci_compression_off;
+
+ heif_context *ctx = heif_context_alloc();
+
+ heif_encoder *encoder;
+ heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_encoding_options *options = heif_encoding_options_alloc();
+ options->macOS_compatibility_workaround_no_nclx_profile = true;
+
+ heif_image_handle *tiled_image;
+ err = heif_context_add_empty_unci_image(ctx, ¶ms, options, prototype, &tiled_image);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_error tile_err = heif_context_add_image_tile(ctx, tiled_image, 0, 0, tile, encoder);
+
+ heif_image_handle_release(tiled_image);
+ heif_encoding_options_free(options);
+ heif_encoder_release(encoder);
+ heif_context_free(ctx);
+
+ return tile_err;
+ };
+
+ SECTION("component plane larger than the tile") {
+ heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2);
+ // Declares the correct tile size, but its Cb plane is far larger than the
+ // subsampled size the encoder allocates for.
+ heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, 200, 200, TW / 2, TH / 2);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("chroma format differs from the prototype") {
+ heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2);
+ // A well-formed 4:4:4 tile: every plane matches its own image dimensions, so
+ // checking the tile against itself is not enough. The encoder still assumes
+ // the 4:2:0 subsampling of the prototype.
+ heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_444, TW, TH, TW, TH);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("tile has fewer components than the prototype") {
+ heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_444, TW, TH, TW, TH);
+
+ heif_image *tile;
+ heif_error err = heif_image_create(TW, TH, heif_colorspace_monochrome, heif_chroma_monochrome, &tile);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("planar tile for an interleaved prototype") {
+ heif_image *prototype;
+ heif_error err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &prototype);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(prototype, heif_channel_interleaved, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image *tile;
+ err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_444, &tile);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_R, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_G, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_B, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("interleaved plane smaller than the image") {
+ heif_image *prototype;
+ heif_error err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &prototype);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(prototype, heif_channel_interleaved, TW, TH, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ // The interleaved plane is not listed by get_used_planar_component_ids(), so a
+ // check that only walks the planar components does not see this at all.
+ heif_image *tile;
+ err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &tile);
+ REQUIRE(err.code == heif_error_Ok);
+ err = heif_image_add_plane(tile, heif_channel_interleaved, 2, 2, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("tile size differs from the tile size of the image") {
+ heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2);
+ // Consistent in itself and matching the prototype configuration, but not the
+ // tile size of the image. It would be written to a wrong offset.
+ heif_image *tile = createImage_YCbCr_customPlanes(TW / 2, TH / 2, heif_chroma_420, TW / 4, TH / 4, TW / 4, TH / 4);
+
+ REQUIRE(add_tile(prototype, tile).code != heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+
+ SECTION("a matching tile is still accepted") {
+ heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2);
+ heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2);
+
+ REQUIRE(add_tile(prototype, tile).code == heif_error_Ok);
+
+ heif_image_release(tile);
+ heif_image_release(prototype);
+ }
+}