Commit 770b0d8d for libheif
commit 770b0d8d7e1e57b0f4d2b8dc53ff3f82b712fbac
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Oct 5 03:44:52 2026 +0200
Refuse decoded images and grid tiles whose bit depth contradicts the image handle (GHSA-vv35-6hxg-95x8)
The bit depth that the image handle reports could differ from the bit
depth of the image that is decoded from it. An application that reads the
planes of the decoded image with the bit depth from the handle, e.g.
16-bit samples from a plane of 8-bit samples, then reads out of bounds
(GHSA-vv35-6hxg-95x8). A file could cause this in two ways.
Configuration box: heif_image_handle_get_luma_bits_per_pixel() and
heif_image_handle_get_chroma_bits_per_pixel() take the bit depth from the
configuration of the item, e.g. 'hvcC' or 'av1C', while the decoded image
gets it from the bitstream. The decoded image was checked against the
signaled size, but not against the signaled bit depth.
ImageItem::decode_image() now does both. The chroma depth is only compared
when Cb and Cr have the same depth, since an 'unci' image can have two
different ones, which the single value of the handle cannot describe.
Grid: the canvas is a clone of the first decoded tile, and
copy_image_to() refuses a tile with another bit depth. Since v1.19.0 the
caller discarded that error, so the tile was left out, its area stayed
zero, and decoding reported success. As the tiles are decoded in parallel,
the first decoded tile is the one of whichever thread finishes first, so
the bit depth of the decoded image changed from run to run. The interface
that decodes a single tile handed out a tile of another format without
any check.
ImageItem_Grid now stores the description of the first tile that is
decoded, together with its colorspace and chroma format, and compares
every tile that is decoded afterwards with it, for the whole image and
for single tiles. The tiles have to have the same colorspace and chroma
format and the same components in the same order, with the same channel,
component type, datatype and bit depth. This also covers 'unci' tiles
with arbitrary components. Which tile is decoded first does not matter:
a tile of another format does not match it either way. The error of
copy_image_to() is returned as well.
Consequences for grids that were decoded before:
- A grid in which only some of the tiles have an alpha plane is refused.
Whether its decoded image had an alpha plane depended on timing.
- A tile that lies completely outside of the image is still skipped. That
was a side effect of the discarded error since v1.19.0 and is explicit
now.
A TODO at ImageItem_Grid::TileFormat describes how the colorspace and the
chroma format should move into ImageDescription, with a function that
compares two descriptions, so that the struct is not needed anymore.
Decoding the 198 files of the test corpus, tests/data and examples gives
the same result with and without this change.
diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc
index b22a2dab..f297ec7c 100644
--- a/libheif/image-items/grid.cc
+++ b/libheif/image-items/grid.cc
@@ -540,6 +540,11 @@ Error ImageItem_Grid::decode_and_paste_tile_image(heif_item_id tileID, uint32_t
tile_img = *decodeResult;
+ // All tiles have to have the same format. The first decoded tile defines it.
+ if (Error err = check_tile_format(*tile_img)) {
+ return err;
+ }
+
uint32_t w = get_grid_spec().get_width();
uint32_t h = get_grid_spec().get_height();
@@ -612,8 +617,19 @@ Error ImageItem_Grid::decode_and_paste_tile_image(heif_item_id tileID, uint32_t
"Image tile has different chroma format than combined image"};
}
+ // The grid may have more rows or columns of tiles than the output image needs. A tile
+ // that lies completely outside of the image has nothing to contribute.
+ if (x0 >= inout_image->get_width() || y0 >= inout_image->get_height()) {
+ return progress_and_return_ok(options, progress_counter);
+ }
- inout_image->copy_image_to(tile_img, x0, y0);
+ // copy_image_to() refuses a tile whose planes have another bit depth than the canvas.
+ // Its error used to be discarded: such a tile was left out, its area stayed zero, and
+ // decoding reported success (GHSA-vv35-6hxg-95x8). check_tile_format() above finds such
+ // a tile already.
+ if (Error err = inout_image->copy_image_to(tile_img, x0, y0)) {
+ return err;
+ }
return progress_and_return_ok(options, progress_counter);
}
@@ -644,7 +660,75 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem_Grid::decode_grid_tile(const h
// Decode the tile like the full grid decoding does. This includes the transformations
// of the tile image and its alpha image. With decode_compressed_image(), a tile that
// has an alpha image of its own would be decoded without alpha channel.
- return tile_item->decode_image(options, false, 0, 0, std::move(decode_state));
+ auto tileResult = tile_item->decode_image(options, false, 0, 0, std::move(decode_state));
+ if (!tileResult) {
+ return tileResult;
+ }
+
+ // A single tile is handed out as it is. It has to have the format of the other tiles,
+ // like the tiles that are copied into the canvas when the whole grid is decoded.
+ if (Error error = check_tile_format(**tileResult)) {
+ return error;
+ }
+
+ return tileResult;
+}
+
+
+Error ImageItem_Grid::check_tile_format(const HeifPixelImage& tile_img) const
+{
+ std::lock_guard<std::mutex> lock(m_reference_tile_format_mutex);
+
+ if (!m_reference_tile_format) {
+ m_reference_tile_format = TileFormat{tile_img.get_colorspace(),
+ tile_img.get_chroma_format(),
+ static_cast<const ImageDescription&>(tile_img)};
+ return Error::Ok;
+ }
+
+ const TileFormat& reference = *m_reference_tile_format;
+
+ if (tile_img.get_colorspace() != reference.colorspace ||
+ tile_img.get_chroma_format() != reference.chroma) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Wrong_tile_image_chroma_format,
+ "Image tile has different colorspace or chroma format than the other tiles of the grid"};
+ }
+
+ // The tiles have to consist of the same components in the same order. This also holds
+ // for 'unci' tiles, which can have any number of components of any type. The size of the
+ // components is not compared here: the tile sizes are checked on their own.
+
+ const std::vector<ComponentDescription>& reference_components = reference.description.get_component_descriptions();
+ const std::vector<ComponentDescription>& components = tile_img.get_component_descriptions();
+
+ if (components.size() != reference_components.size()) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Invalid_grid_data,
+ "Image tile has a different number of components than the other tiles of the grid"};
+ }
+
+ for (size_t i = 0; i < components.size(); i++) {
+ const ComponentDescription& c = components[i];
+ const ComponentDescription& ref = reference_components[i];
+
+ if (c.channel != ref.channel ||
+ c.component_type != ref.component_type ||
+ c.datatype != ref.datatype ||
+ c.has_data_plane != ref.has_data_plane) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Invalid_grid_data,
+ "Image tile has different components than the other tiles of the grid"};
+ }
+
+ if (c.bit_depth != ref.bit_depth) {
+ return {heif_error_Invalid_input,
+ heif_suberror_Wrong_tile_image_pixel_depth,
+ "Image tile has different bit depth than the other tiles of the grid"};
+ }
+ }
+
+ return Error::Ok;
}
diff --git a/libheif/image-items/grid.h b/libheif/image-items/grid.h
index 39f7edf7..1816d697 100644
--- a/libheif/image-items/grid.h
+++ b/libheif/image-items/grid.h
@@ -22,9 +22,12 @@
#define LIBHEIF_IMAGEITEM_GRID_H
#include "image_item.h"
+#include "image/image_description.h"
#include <vector>
#include <string>
#include <memory>
+#include <mutex>
+#include <optional>
#include <set>
@@ -163,6 +166,51 @@ private:
ImageGrid m_grid_spec;
std::vector<heif_item_id> m_grid_tile_ids;
+ // --- the format that all tiles of the grid have to share
+
+ // All tiles of a grid have to decode to images of the same format. The first tile that
+ // is decoded (in whatever order, and through whichever decoding interface) stores its
+ // description here, and every tile that is decoded afterwards is compared with it.
+ //
+ // TODO: move the colorspace and the chroma format into ImageDescription.
+ // They are members of HeifPixelImage only, although they are the top level of the
+ // structure that ImageDescription describes: without them, a list of components does
+ // not tell whether the components are subsampled or interleaved. That is why this
+ // struct has to carry them next to the description. With both in ImageDescription,
+ // TileFormat becomes a plain ImageDescription, and check_tile_format() becomes a
+ // function of ImageDescription that compares two descriptions for having the same
+ // format. That function can then also compare a decoded image with what its image item
+ // advertises, of which ImageItem::check_decoded_image_bit_depth() covers only the luma
+ // and chroma bit depths today (not, e.g., a configuration box that claims another
+ // chroma format than the bitstream has).
+ // Things to take care of:
+ // - ImageDescription::copy_metadata_from() must not copy them. It copies the metadata
+ // (colour profile, light levels, ...), but not the component list, because the
+ // target image can have another layout, e.g. after a colour conversion. Colorspace
+ // and chroma format belong to that structural part.
+ // - An image item knows two colorspaces: the coded one, and the preferred decoding
+ // colorspace, which proposes RGB for matrix_coefficients=0 while the decoded image is
+ // still tagged as YCbCr. The description has to hold the coded one, otherwise it
+ // does not match the decoded image.
+ // - Every implementation of populate_component_descriptions() has to set them:
+ // ImageItem, ImageItem_iden, ImageItem_Grid, ImageItem_uncompressed, ImageItem_Tiled
+ // and ImageItem_Overlay. Where the format is not known, they stay undefined, and a
+ // comparison has to treat that as unknown, not as a mismatch.
+ // - In HeifPixelImage, the two values are tied to the planes of the image and are set
+ // in create() only. They should not become freely settable through the base class.
+ struct TileFormat
+ {
+ heif_colorspace colorspace = heif_colorspace_undefined;
+ heif_chroma chroma = heif_chroma_undefined;
+ ImageDescription description;
+ };
+
+ mutable std::mutex m_reference_tile_format_mutex; // tiles are decoded in parallel
+ mutable std::optional<TileFormat> m_reference_tile_format;
+
+ // Stores the format of the first decoded tile, or compares the tile with the stored format.
+ Error check_tile_format(const HeifPixelImage& tile_img) const;
+
heif_orientation m_grid_orientation = heif_orientation_normal;
heif_encoding_options* m_tile_encoding_options = nullptr;
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 5d826516..303ded2c 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -1260,6 +1260,12 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
return err;
}
+ // --- validate the decoded image against the signaled bit depth
+
+ if (Error err = check_decoded_image_bit_depth(*img)) {
+ return err;
+ }
+
std::shared_ptr<HeifFile> file = m_heif_context->get_heif_file();
@@ -1649,6 +1655,44 @@ Error ImageItem::check_decoded_image_size(const HeifPixelImage& img,
}
+Error ImageItem::check_decoded_image_bit_depth(const HeifPixelImage& img) const
+{
+ // The bit depths that the image handle reports (heif_image_handle_get_luma_bits_per_pixel()
+ // and ..._chroma_...) are taken from the configuration of the item, e.g. the 'hvcC' box,
+ // or from the first tile of a grid. The decoded image gets its bit depth from the
+ // bitstream. A file can make the two disagree. An application that reads the planes of
+ // the decoded image with the bit depth it got from the handle would then read out of
+ // bounds, so the decoded image must not contradict the handle (GHSA-vv35-6hxg-95x8).
+
+ int luma_bpp = get_luma_bits_per_pixel();
+ int chroma_bpp = get_chroma_bits_per_pixel();
+
+ bool mismatch = false;
+
+ if (luma_bpp > 0 && img.has_channel(heif_channel_Y) &&
+ img.get_bits_per_pixel(heif_channel_Y) != luma_bpp) {
+ mismatch = true;
+ }
+
+ // The handle reports a single bit depth for both chroma planes. An 'unci' image can have
+ // Cb and Cr planes of different depths, which a single value cannot describe, so the
+ // chroma depth is only compared when both planes have the same.
+ if (chroma_bpp > 0 && img.has_channel(heif_channel_Cb) && img.has_channel(heif_channel_Cr) &&
+ img.get_bits_per_pixel(heif_channel_Cb) == img.get_bits_per_pixel(heif_channel_Cr) &&
+ img.get_bits_per_pixel(heif_channel_Cb) != chroma_bpp) {
+ mismatch = true;
+ }
+
+ if (mismatch) {
+ return Error{heif_error_Invalid_input,
+ heif_suberror_Unspecified,
+ "Decoded image does not have the bit depth signaled in the file."};
+ }
+
+ return Error::Ok;
+}
+
+
heif_image_tiling ImageItem::get_heif_image_tiling() const
{
// --- Return a dummy tiling consisting of only a single tile for the whole image
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 100ede4c..6e1a25ab 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -409,6 +409,10 @@ public:
bool decode_tile_only,
uint32_t tile_x0, uint32_t tile_y0) const;
+ // Checks that the decoded image has the bit depths that the image handle reports
+ // (get_luma_bits_per_pixel() and get_chroma_bits_per_pixel()).
+ Error check_decoded_image_bit_depth(const HeifPixelImage& img) const;
+
Result<std::vector<std::shared_ptr<Box>>> get_properties() const;
bool has_essential_property_other_than(const std::set<uint32_t>&) const;
diff --git a/libheif/image/image_description.h b/libheif/image/image_description.h
index cc9a4ff4..9c2c8180 100644
--- a/libheif/image/image_description.h
+++ b/libheif/image/image_description.h
@@ -153,6 +153,9 @@ struct ComponentDescription
};
+// TODO: the colorspace and the chroma format of the image should be part of this class,
+// together with a function that compares two descriptions for having the same format.
+// See the notes at ImageItem_Grid::TileFormat (image-items/grid.h).
class ImageDescription
{
public:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1e9b994d..94f88de5 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -113,6 +113,7 @@ add_libheif_test(alpha_cycle_deadlock)
add_libheif_test(alpha_composite_decode)
add_libheif_test(encode_plane_layout)
add_libheif_test(encode_handle_queries)
+add_libheif_test(decoded_bit_depth_mismatch)
add_libheif_test(write_to_file)
add_libheif_test(parallel_grid_deadlock)
# The deadlock regression tests decode on a worker thread guarded by a timeout.
@@ -172,6 +173,7 @@ if (WITH_UNCOMPRESSED_CODEC)
add_libheif_test(uncompressed_mixed_chroma_depth_overflow)
add_libheif_test(uncompressed_mixed_interleave_bit_depth)
add_libheif_test(uncompressed_grid_wide_alpha)
+ add_libheif_test(uncompressed_grid_tile_mismatch)
add_libheif_test(uncompressed_mixed_chroma_depth_colorconv)
add_libheif_test(uncompressed_alpha_composite_depth_mismatch)
add_libheif_test(uncompressed_mixed_rgb_depth_to_sdr)
diff --git a/tests/decoded_bit_depth_mismatch.cc b/tests/decoded_bit_depth_mismatch.cc
new file mode 100644
index 00000000..d744f63e
--- /dev/null
+++ b/tests/decoded_bit_depth_mismatch.cc
@@ -0,0 +1,225 @@
+/*
+ libheif unit tests
+
+ MIT License
+
+ Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+// The bit depth that heif_image_handle_get_luma_bits_per_pixel() reports is taken from the
+// configuration box of the image item ('hvcC', 'av1C'). The decoded image gets its bit depth
+// from the bitstream. A file in which the two disagree was decoded without an error, and the
+// decoded image had another bit depth than the handle reported. An application that reads
+// the planes of the decoded image with the bit depth from the handle (16-bit samples from a
+// plane with 8-bit samples) reads out of bounds (GHSA-vv35-6hxg-95x8). Such a file is
+// refused now, like a file whose decoded image does not have the signaled size.
+//
+// The test encodes an 8-bit image and changes the configuration box so that it claims 10 bit.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
+#include <string>
+#include <vector>
+
+namespace {
+
+constexpr int W = 64;
+constexpr int H = 64;
+
+heif_error write_to_vector(heif_context*, const void* data, size_t size, void* userdata)
+{
+ auto* out = static_cast<std::vector<uint8_t>*>(userdata);
+ const auto* bytes = static_cast<const uint8_t*>(data);
+ out->insert(out->end(), bytes, bytes + size);
+ return heif_error{heif_error_Ok, heif_suberror_Unspecified, "Success"};
+}
+
+
+std::vector<uint8_t> encode_8bit_image(heif_compression_format format)
+{
+ heif_image* img = nullptr;
+ heif_error err = heif_image_create(W, H, heif_colorspace_YCbCr, heif_chroma_420, &img);
+ REQUIRE(err.code == heif_error_Ok);
+
+ for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) {
+ int w = (channel == heif_channel_Y) ? W : W / 2;
+ int h = (channel == heif_channel_Y) ? H : H / 2;
+ err = heif_image_add_plane(img, channel, w, h, 8);
+ REQUIRE(err.code == heif_error_Ok);
+
+ size_t stride = 0;
+ uint8_t* p = heif_image_get_plane2(img, channel, &stride);
+ REQUIRE(p != nullptr);
+ for (int y = 0; y < h; y++) {
+ memset(p + y * stride, 0x60, w);
+ }
+ }
+
+ heif_context* ctx = heif_context_alloc();
+ heif_encoder* encoder = nullptr;
+ err = heif_context_get_encoder_for_format(ctx, format, &encoder);
+ REQUIRE(err.code == heif_error_Ok);
+
+ err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr);
+ INFO("encode: " << err.message);
+ REQUIRE(err.code == heif_error_Ok);
+
+ std::vector<uint8_t> file;
+ heif_writer writer;
+ writer.writer_api_version = 1;
+ writer.write = write_to_vector;
+ err = heif_context_write(ctx, &writer, &file);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_encoder_release(encoder);
+ heif_context_free(ctx);
+ heif_image_release(img);
+
+ return file;
+}
+
+
+// Makes the configuration box claim a bit depth of 10 for luma and chroma.
+void claim_10_bit(std::vector<uint8_t>& file, heif_compression_format format)
+{
+ const char* fourcc = (format == heif_compression_HEVC) ? "hvcC" : "av1C";
+
+ for (size_t i = 0; i + 4 + 19 < file.size(); i++) {
+ if (memcmp(&file[i], fourcc, 4) != 0) {
+ continue;
+ }
+
+ uint8_t* config = &file[i + 4];
+
+ if (format == heif_compression_HEVC) {
+ // HEVCDecoderConfigurationRecord: bitDepthLumaMinus8 and bitDepthChromaMinus8 are the
+ // lower three bits of the bytes 17 and 18.
+ config[17] = static_cast<uint8_t>((config[17] & 0xF8) | 2);
+ config[18] = static_cast<uint8_t>((config[18] & 0xF8) | 2);
+ }
+ else {
+ // AV1CodecConfigurationRecord: high_bitdepth is the second bit of the third byte.
+ config[2] = static_cast<uint8_t>(config[2] | 0x40);
+ }
+ return;
+ }
+
+ FAIL("configuration box not found");
+}
+
+
+struct Decoded
+{
+ int handle_luma_bits = 0;
+ heif_error_code code = heif_error_Ok;
+ std::string message;
+ bool has_image = false;
+ int image_luma_bits = 0;
+};
+
+Decoded decode(const std::vector<uint8_t>& file, heif_colorspace colorspace, heif_chroma chroma)
+{
+ Decoded result;
+
+ heif_context* ctx = heif_context_alloc();
+ heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
+ INFO("read: " << err.message);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle* handle = nullptr;
+ err = heif_context_get_primary_image_handle(ctx, &handle);
+ REQUIRE(err.code == heif_error_Ok);
+
+ result.handle_luma_bits = heif_image_handle_get_luma_bits_per_pixel(handle);
+
+ heif_image* img = nullptr;
+ err = heif_decode_image(handle, &img, colorspace, chroma, nullptr);
+ result.code = err.code;
+ result.message = err.message ? err.message : "";
+ result.has_image = (img != nullptr);
+
+ if (img) {
+ if (heif_image_has_channel(img, heif_channel_Y)) {
+ result.image_luma_bits = heif_image_get_bits_per_pixel_range(img, heif_channel_Y);
+ }
+ heif_image_release(img);
+ }
+
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ return result;
+}
+
+} // namespace
+
+
+TEST_CASE("an image whose decoded bit depth contradicts the image handle is refused")
+{
+ int num_formats_tested = 0;
+
+ for (heif_compression_format format : {heif_compression_AV1, heif_compression_HEVC}) {
+ if (!heif_have_encoder_for_format(format) || !heif_have_decoder_for_format(format)) {
+ continue;
+ }
+ num_formats_tested++;
+
+ INFO("format: " << (format == heif_compression_HEVC ? "HEVC" : "AV1"));
+
+ std::vector<uint8_t> file = encode_8bit_image(format);
+
+ // --- control: the unmodified file
+
+ Decoded control = decode(file, heif_colorspace_undefined, heif_chroma_undefined);
+ INFO("control: " << control.message);
+ REQUIRE(control.code == heif_error_Ok);
+ CHECK(control.handle_luma_bits == 8);
+ CHECK(control.image_luma_bits == 8);
+
+ // --- the configuration box claims 10 bit, the bitstream has 8 bit
+
+ claim_10_bit(file, format);
+
+ Decoded native = decode(file, heif_colorspace_undefined, heif_chroma_undefined);
+ INFO("decoding without conversion: " << native.message);
+ CHECK(native.handle_luma_bits == 10);
+
+ // The decoded image must never have another bit depth than the handle reports.
+ if (native.has_image) {
+ CHECK(native.image_luma_bits == native.handle_luma_bits);
+ }
+ CHECK(native.code != heif_error_Ok);
+ CHECK(!native.has_image);
+
+ // A conversion does not hide the contradiction.
+ Decoded converted = decode(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB);
+ INFO("decoding to RGB: " << converted.message);
+ CHECK(converted.code != heif_error_Ok);
+ CHECK(!converted.has_image);
+ }
+
+ if (num_formats_tested == 0) {
+ SKIP("neither an AV1 nor an HEVC encoder and decoder available");
+ }
+}
diff --git a/tests/uncompressed_grid_tile_mismatch.cc b/tests/uncompressed_grid_tile_mismatch.cc
new file mode 100644
index 00000000..ef6ea23a
--- /dev/null
+++ b/tests/uncompressed_grid_tile_mismatch.cc
@@ -0,0 +1,589 @@
+/*
+ libheif unit tests
+
+ MIT License
+
+ Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+// The canvas of a 'grid' image takes its planes and bit depths from the first decoded tile,
+// and the other tiles are copied into it. Two things went wrong when the tiles of a grid do
+// not have the same format (GHSA-vv35-6hxg-95x8):
+//
+// - The function that copies a tile refuses a tile with another bit depth, but its error was
+// discarded. The tile was left out, its area stayed zero, and decoding reported success.
+// - With parallel tile decoding, "the first decoded tile" is the tile of whichever thread
+// finishes first. The format of the decoded image, and which of the tiles was left out,
+// changed from run to run.
+//
+// The grid item now remembers the description of the first tile that is decoded, and every
+// tile that is decoded afterwards has to have the same format: colorspace, chroma format, and
+// the same components with the same datatypes and bit depths. A tile that differs is an
+// error. Which tile is the first one still depends on timing, but that no longer matters:
+// whichever it is, a tile of another format does not match it.
+//
+// This also covers the interface that decodes single tiles, which has no canvas to compare
+// a tile with: the tiles that are decoded from one context are compared with each other.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "test_utils.h"
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+namespace {
+
+constexpr uint32_t TILE_W = 16;
+constexpr uint32_t TILE_H = 16;
+
+// component types of 'cmpd'
+constexpr uint16_t MONO = 0, Y = 1, CB = 2, CR = 3, R = 4, G = 5, B = 6, A = 7;
+
+struct Component
+{
+ uint16_t type;
+ uint8_t bit_depth; // 8 or 16
+};
+
+struct Tile
+{
+ std::vector<Component> components;
+ uint8_t fill; // every byte of the tile data
+};
+
+
+// A 'grid' of 'unci' tiles in one row. 'output_width' is the width of the grid image.
+std::vector<uint8_t> build_grid_file(const std::vector<Tile>& tiles, uint32_t output_width)
+{
+ const uint16_t num_tiles = static_cast<uint16_t>(tiles.size());
+
+ std::vector<uint8_t> ftyp_payload;
+ append_fourcc(ftyp_payload, "mif1");
+ put_u32_be(ftyp_payload, 0);
+ append_fourcc(ftyp_payload, "mif1");
+ append_fourcc(ftyp_payload, "heic");
+ auto ftyp = make_box("ftyp", ftyp_payload);
+
+ std::vector<uint8_t> hdlr_payload;
+ put_u32_be(hdlr_payload, 0);
+ append_fourcc(hdlr_payload, "pict");
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ put_u32_be(hdlr_payload, 0);
+ hdlr_payload.push_back(0);
+ auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true);
+
+ std::vector<uint8_t> pitm_payload;
+ put_u16_be(pitm_payload, 1);
+ auto pitm = make_box("pitm", pitm_payload, /*full=*/true);
+
+ // items: 1 = grid, 2... = unci tiles
+ std::vector<uint8_t> iinf_payload;
+ put_u16_be(iinf_payload, static_cast<uint16_t>(1 + num_tiles));
+ for (uint16_t id = 1; id <= 1 + num_tiles; id++) {
+ std::vector<uint8_t> infe_payload;
+ put_u16_be(infe_payload, id);
+ put_u16_be(infe_payload, 0);
+ append_fourcc(infe_payload, id == 1 ? "grid" : "unci");
+ append_cstr(infe_payload, "");
+ append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2));
+ }
+ auto iinf = make_box("iinf", iinf_payload, /*full=*/true);
+
+ std::vector<uint8_t> dimg_payload;
+ put_u16_be(dimg_payload, 1); // from the grid
+ put_u16_be(dimg_payload, num_tiles);
+ for (uint16_t i = 0; i < num_tiles; i++) {
+ put_u16_be(dimg_payload, static_cast<uint16_t>(2 + i));
+ }
+ auto iref = make_box("iref", make_box("dimg", dimg_payload), /*full=*/true);
+
+ // properties: 1 = ispe of the grid, 2 = ispe of a tile, then cmpd and uncC of each tile
+ std::vector<uint8_t> grid_ispe_payload;
+ put_u32_be(grid_ispe_payload, output_width);
+ put_u32_be(grid_ispe_payload, TILE_H);
+
+ std::vector<uint8_t> tile_ispe_payload;
+ put_u32_be(tile_ispe_payload, TILE_W);
+ put_u32_be(tile_ispe_payload, TILE_H);
+
+ std::vector<uint8_t> ipco_payload;
+ append(ipco_payload, make_box("ispe", grid_ispe_payload, /*full=*/true));
+ append(ipco_payload, make_box("ispe", tile_ispe_payload, /*full=*/true));
+
+ for (const Tile& tile : tiles) {
+ const uint32_t num_components = static_cast<uint32_t>(tile.components.size());
+
+ std::vector<uint8_t> cmpd_payload;
+ put_u32_be(cmpd_payload, num_components);
+ for (const Component& c : tile.components) {
+ put_u16_be(cmpd_payload, c.type);
+ }
+
+ std::vector<uint8_t> uncC_payload;
+ put_u32_be(uncC_payload, 0); // profile
+ put_u32_be(uncC_payload, num_components);
+ for (uint16_t idx = 0; idx < num_components; idx++) {
+ put_u16_be(uncC_payload, idx); // component_index
+ uncC_payload.push_back(static_cast<uint8_t>(tile.components[idx].bit_depth - 1)); // component_bit_depth_minus_one
+ uncC_payload.push_back(0); // component_format (unsigned)
+ uncC_payload.push_back(0); // component_align_size
+ }
+ uncC_payload.push_back(0); // sampling_type = no subsampling
+ uncC_payload.push_back(0); // interleave_type = component
+ uncC_payload.push_back(0); // block_size
+ uncC_payload.push_back(0); // flags (big-endian components)
+ put_u32_be(uncC_payload, 0); // pixel_size
+ put_u32_be(uncC_payload, 0); // row_align_size
+ put_u32_be(uncC_payload, 0); // tile_align_size
+ put_u32_be(uncC_payload, 0); // num_tile_cols_minus_one
+ put_u32_be(uncC_payload, 0); // num_tile_rows_minus_one
+
+ append(ipco_payload, make_box("cmpd", cmpd_payload));
+ append(ipco_payload, make_box("uncC", uncC_payload, /*full=*/true));
+ }
+ auto ipco = make_box("ipco", ipco_payload);
+
+ std::vector<uint8_t> ipma_payload;
+ put_u32_be(ipma_payload, 1u + num_tiles); // entry_count
+ put_u16_be(ipma_payload, 1); // the grid
+ ipma_payload.push_back(1);
+ ipma_payload.push_back(0x80 | 1);
+ for (uint16_t i = 0; i < num_tiles; i++) {
+ put_u16_be(ipma_payload, static_cast<uint16_t>(2 + i));
+ ipma_payload.push_back(3);
+ ipma_payload.push_back(0x80 | 2);
+ ipma_payload.push_back(static_cast<uint8_t>(0x80 | (3 + 2 * i)));
+ ipma_payload.push_back(static_cast<uint8_t>(0x80 | (4 + 2 * i)));
+ }
+ auto ipma = make_box("ipma", ipma_payload, /*full=*/true);
+
+ std::vector<uint8_t> iprp_payload;
+ append(iprp_payload, ipco);
+ append(iprp_payload, ipma);
+ auto iprp = make_box("iprp", iprp_payload);
+
+ // --- item data, all in 'idat'
+
+ std::vector<std::vector<uint8_t>> item_data;
+
+ std::vector<uint8_t> grid_data;
+ grid_data.push_back(0); // version
+ grid_data.push_back(0); // flags
+ grid_data.push_back(0); // rows_minus_one
+ grid_data.push_back(static_cast<uint8_t>(num_tiles - 1)); // columns_minus_one
+ put_u16_be(grid_data, static_cast<uint16_t>(output_width));
+ put_u16_be(grid_data, TILE_H);
+ item_data.push_back(grid_data);
+
+ for (const Tile& tile : tiles) {
+ size_t size = 0;
+ for (const Component& c : tile.components) {
+ size += TILE_W * TILE_H * (c.bit_depth / 8);
+ }
+ item_data.emplace_back(size, tile.fill);
+ }
+
+ std::vector<uint8_t> idat_payload;
+ for (const auto& data : item_data) {
+ append(idat_payload, data);
+ }
+ auto idat = make_box("idat", idat_payload);
+
+ std::vector<uint8_t> iloc_payload;
+ put_u16_be(iloc_payload, (4 << 12) | (4 << 8)); // offset_size=4, length_size=4
+ put_u16_be(iloc_payload, static_cast<uint16_t>(item_data.size()));
+ uint32_t offset = 0;
+ for (uint16_t i = 0; i < item_data.size(); i++) {
+ put_u16_be(iloc_payload, static_cast<uint16_t>(1 + i));
+ put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat)
+ put_u16_be(iloc_payload, 0); // data_reference_index
+ put_u16_be(iloc_payload, 1); // extent_count
+ put_u32_be(iloc_payload, offset);
+ put_u32_be(iloc_payload, static_cast<uint32_t>(item_data[i].size()));
+ offset += static_cast<uint32_t>(item_data[i].size());
+ }
+ auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1);
+
+ std::vector<uint8_t> meta_payload;
+ append(meta_payload, hdlr);
+ append(meta_payload, pitm);
+ append(meta_payload, iinf);
+ append(meta_payload, iref);
+ append(meta_payload, iprp);
+ append(meta_payload, iloc);
+ append(meta_payload, idat);
+ auto meta = make_box("meta", meta_payload, /*full=*/true);
+
+ std::vector<uint8_t> file;
+ append(file, ftyp);
+ append(file, meta);
+ return file;
+}
+
+
+// What one decode of the file returned, in a form that can be compared between runs.
+struct Outcome
+{
+ heif_error_code code = heif_error_Ok;
+ heif_suberror_code subcode = heif_suberror_Unspecified;
+ std::string message;
+
+ bool has_alpha = false;
+ int bit_depth = 0; // of the first colour plane
+ std::vector<uint8_t> first_row; // first byte of each sample of the first colour plane
+ std::vector<uint8_t> alpha_row; // first byte of each alpha sample
+
+ bool operator==(const Outcome&) const = default;
+};
+
+
+Outcome decode(const std::vector<uint8_t>& file)
+{
+ Outcome outcome;
+
+ heif_context* ctx = heif_context_alloc();
+ heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
+ INFO("read: " << err.message);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle* handle = nullptr;
+ err = heif_context_get_primary_image_handle(ctx, &handle);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image* img = nullptr;
+ err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+ outcome.code = err.code;
+ outcome.subcode = err.subcode;
+ outcome.message = err.message ? err.message : "";
+
+ if (img) {
+ heif_channel channel = heif_image_has_channel(img, heif_channel_Y) ? heif_channel_Y : heif_channel_R;
+ REQUIRE(heif_image_has_channel(img, channel));
+
+ auto first_bytes_of_row = [img](heif_channel ch) {
+ size_t stride = 0;
+ const uint8_t* p = heif_image_get_plane_readonly2(img, ch, &stride);
+ REQUIRE(p != nullptr);
+ int w = heif_image_get_width(img, ch);
+ int bytes_per_sample = (heif_image_get_bits_per_pixel_range(img, ch) + 7) / 8;
+ std::vector<uint8_t> row;
+ for (int x = 0; x < w; x++) {
+ row.push_back(p[static_cast<size_t>(x) * bytes_per_sample]);
+ }
+ return row;
+ };
+
+ outcome.bit_depth = heif_image_get_bits_per_pixel_range(img, channel);
+ outcome.first_row = first_bytes_of_row(channel);
+
+ outcome.has_alpha = heif_image_has_channel(img, heif_channel_Alpha);
+ if (outcome.has_alpha) {
+ outcome.alpha_row = first_bytes_of_row(heif_channel_Alpha);
+ }
+
+ heif_image_release(img);
+ }
+
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ return outcome;
+}
+
+
+// Decodes the file several times, because the tiles are decoded in parallel: the result
+// must not depend on which tile is finished first and creates the canvas.
+Outcome decode_repeatedly(const std::vector<uint8_t>& file)
+{
+ Outcome first = decode(file);
+ for (int i = 0; i < 20; i++) {
+ Outcome again = decode(file);
+ INFO("run " << i << ": " << again.message);
+ REQUIRE(again == first);
+ }
+ return first;
+}
+
+
+// Decodes the given tiles of the grid, one after the other, through the tile interface of
+// one context.
+std::vector<Outcome> decode_tiles(const std::vector<uint8_t>& file, std::initializer_list<uint32_t> tiles_x)
+{
+ std::vector<Outcome> outcomes;
+
+ heif_context* ctx = heif_context_alloc();
+ heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_image_handle* handle = nullptr;
+ err = heif_context_get_primary_image_handle(ctx, &handle);
+ REQUIRE(err.code == heif_error_Ok);
+
+ for (uint32_t tile_x : tiles_x) {
+ Outcome outcome;
+
+ heif_image* img = nullptr;
+ err = heif_image_handle_decode_image_tile(handle, &img, heif_colorspace_undefined, heif_chroma_undefined,
+ nullptr, tile_x, 0);
+ outcome.code = err.code;
+ outcome.subcode = err.subcode;
+ outcome.message = err.message ? err.message : "";
+
+ if (img) {
+ heif_channel channel = heif_image_has_channel(img, heif_channel_Y) ? heif_channel_Y : heif_channel_R;
+ REQUIRE(heif_image_has_channel(img, channel));
+
+ size_t stride = 0;
+ const uint8_t* p = heif_image_get_plane_readonly2(img, channel, &stride);
+ REQUIRE(p != nullptr);
+ outcome.bit_depth = heif_image_get_bits_per_pixel_range(img, channel);
+ int bytes_per_sample = (outcome.bit_depth + 7) / 8;
+ for (int x = 0; x < heif_image_get_width(img, channel); x++) {
+ outcome.first_row.push_back(p[static_cast<size_t>(x) * bytes_per_sample]);
+ }
+ outcome.has_alpha = heif_image_has_channel(img, heif_channel_Alpha);
+
+ heif_image_release(img);
+ }
+
+ outcomes.push_back(outcome);
+ }
+
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ return outcomes;
+}
+
+
+std::vector<uint8_t> row_of(std::initializer_list<uint8_t> tile_values)
+{
+ std::vector<uint8_t> row;
+ for (uint8_t v : tile_values) {
+ row.insert(row.end(), TILE_W, v);
+ }
+ return row;
+}
+
+} // namespace
+
+
+TEST_CASE("grid with tiles of the same format")
+{
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ REQUIRE(outcome.code == heif_error_Ok);
+ CHECK(outcome.bit_depth == 8);
+ CHECK(outcome.first_row == row_of({0xAA, 0xBB}));
+}
+
+
+TEST_CASE("grid with tiles of different bit depths is refused")
+{
+ SECTION("8 bit, then 16 bit") {
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 16}}, 0xBB}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth);
+ }
+
+ SECTION("16 bit, then 8 bit") {
+ auto file = build_grid_file({{{{MONO, 16}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth);
+ }
+
+ SECTION("only one plane differs") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{R, 8}, {G, 8}, {B, 16}}, 0xBB}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth);
+ }
+}
+
+
+TEST_CASE("grid with tiles of different colorspaces is refused")
+{
+ // RGB and YCbCr 4:4:4 have the same chroma format but no plane in common.
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{Y, 8}, {CB, 8}, {CR, 8}}, 0xBB}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_chroma_format);
+}
+
+
+TEST_CASE("grid with tiles that differ in their components is refused")
+{
+ // Only one of the tiles has an alpha plane. Whether the decoded image had an alpha plane
+ // used to depend on which tile was decoded first.
+ SECTION("first tile with alpha") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}}, 0x80}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Invalid_grid_data);
+ }
+
+ SECTION("second tile with alpha") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x80}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ CHECK(outcome.code == heif_error_Invalid_input);
+ CHECK(outcome.subcode == heif_suberror_Invalid_grid_data);
+ }
+
+ SECTION("all tiles with alpha") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x80}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ REQUIRE(outcome.code == heif_error_Ok);
+ CHECK(outcome.first_row == row_of({0x40, 0x80}));
+ REQUIRE(outcome.has_alpha);
+ CHECK(outcome.alpha_row == row_of({0x40, 0x80}));
+ }
+}
+
+
+TEST_CASE("grid with a tile completely outside of the image")
+{
+ // Three tiles in a row, but the image is only two tiles wide. The third tile has nothing
+ // to contribute. This is not an error.
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}, {{{MONO, 8}}, 0xCC}}, 2 * TILE_W);
+
+ Outcome outcome = decode_repeatedly(file);
+ INFO("decode: " << outcome.message);
+ REQUIRE(outcome.code == heif_error_Ok);
+ CHECK(outcome.first_row == row_of({0xAA, 0xBB}));
+}
+
+
+TEST_CASE("decoding single grid tiles refuses a tile that does not have the format of the other tiles")
+{
+ SECTION("tiles of the same format") {
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W);
+
+ auto tiles = decode_tiles(file, {0, 1});
+ INFO("tile 0: " << tiles[0].message << ", tile 1: " << tiles[1].message);
+ REQUIRE(tiles[0].code == heif_error_Ok);
+ CHECK(tiles[0].first_row == row_of({0xAA}));
+ REQUIRE(tiles[1].code == heif_error_Ok);
+ CHECK(tiles[1].first_row == row_of({0xBB}));
+ }
+
+ SECTION("another bit depth") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{R, 8}, {G, 8}, {B, 16}}, 0xBB}}, 2 * TILE_W);
+
+ // The tile that is decoded first is the one the others are compared with.
+ for (bool reversed : {false, true}) {
+ auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1});
+ INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message);
+ CHECK(tiles[0].code == heif_error_Ok);
+ CHECK(tiles[1].code == heif_error_Invalid_input);
+ CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_pixel_depth);
+ }
+ }
+
+ SECTION("another colorspace") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{Y, 8}, {CB, 8}, {CR, 8}}, 0xBB}}, 2 * TILE_W);
+
+ for (bool reversed : {false, true}) {
+ auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1});
+ INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message);
+ CHECK(tiles[0].code == heif_error_Ok);
+ CHECK(tiles[1].code == heif_error_Invalid_input);
+ CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_chroma_format);
+ }
+ }
+
+ SECTION("other components") {
+ auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}}, 0x80}}, 2 * TILE_W);
+
+ for (bool reversed : {false, true}) {
+ auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1});
+ INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message);
+ CHECK(tiles[0].code == heif_error_Ok);
+ CHECK(tiles[1].code == heif_error_Invalid_input);
+ CHECK(tiles[1].subcode == heif_suberror_Invalid_grid_data);
+ }
+ }
+
+ SECTION("a tile whose bit depth contradicts the image handle") {
+ // The handle of the grid reports the bit depth of its first tile (8 bit). The second
+ // tile has 16 bit. It is refused even when it is the only tile that is decoded, because
+ // a decoded image must not have another bit depth than the handle reports.
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 16}}, 0xBB}}, 2 * TILE_W);
+
+ auto tiles = decode_tiles(file, {1});
+ INFO("tile 1: " << tiles[0].message);
+ CHECK(tiles[0].code == heif_error_Invalid_input);
+
+ tiles = decode_tiles(file, {0, 1});
+ INFO("tile 0: " << tiles[0].message << ", tile 1: " << tiles[1].message);
+ REQUIRE(tiles[0].code == heif_error_Ok);
+ CHECK(tiles[0].bit_depth == 8);
+ CHECK(tiles[1].code == heif_error_Invalid_input);
+ CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_pixel_depth);
+ }
+
+ SECTION("decoding the whole image after a single tile") {
+ auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W);
+
+ // The stored description of a tile must not get in the way of later decoding.
+ heif_context* ctx = heif_context_alloc();
+ heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+ heif_image_handle* handle = nullptr;
+ REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok);
+
+ for (int round = 0; round < 3; round++) {
+ heif_image* img = nullptr;
+ err = heif_image_handle_decode_image_tile(handle, &img, heif_colorspace_undefined, heif_chroma_undefined,
+ nullptr, 1, 0);
+ REQUIRE(err.code == heif_error_Ok);
+ heif_image_release(img);
+
+ img = nullptr;
+ err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+ INFO("decode: " << err.message);
+ REQUIRE(err.code == heif_error_Ok);
+ heif_image_release(img);
+ }
+
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+ }
+}