Commit 6e7c728f for libheif
commit 6e7c728f3015fc02bea9a30a42e6834bee71c752
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Oct 5 00:06:03 2026 +0200
Read 'unci' mixed-interleave chroma samples with their component bit depth (GHSA-v5rj-g4wv-j5w3)
In mixed interleave mode (ISO/IEC 23001-17, 5.2.1.6.4), the Cb and Cr
samples are stored like the components of a pixel in pixel interleave
mode: each value is coded with component_bit_depth bits, or, when
component_align_size is set, as a byte-aligned word with padding in the
upper bits.
The decoder read each chroma sample with the width of the plane's storage
word instead (16 bits for a 12-bit component). The bits of the following
sample, or the padding bits, ended up in the upper bits of the value, so
the chroma planes contained samples of up to 65535 in a plane that is
declared as, e.g., 12 bit (GHSA-v5rj-g4wv-j5w3). Code that relies on the
bit depth of a decoded image can then be made to read out of bounds, as
libsharpyuv did in GHSA-q7mw-2fmm-5q94. Well-formed files with a chroma
bit depth other than 8 or 16 were decoded with wrong chroma values.
Read the chroma samples with the bit depth of their component, skip the
alignment padding, and end the rows on a byte boundary.
get_tile_data_sizes() added up the two chroma components as if they were
stored separately. It now computes the size of the interleaved rows in the
same way as processTile() reads them. row_align_size, which was not
implemented for this mode, is handled as well.
The new test writes the sample data of 18 layouts with its own bit writer
and requires that every sample is decoded exactly, with the padding bits
set to zero and to one.
diff --git a/libheif/codecs/uncompressed/unc_decoder_legacybase.cc b/libheif/codecs/uncompressed/unc_decoder_legacybase.cc
index a33a494d..e2558d31 100644
--- a/libheif/codecs/uncompressed/unc_decoder_legacybase.cc
+++ b/libheif/codecs/uncompressed/unc_decoder_legacybase.cc
@@ -168,6 +168,20 @@ unc_decoder_legacybase::ChannelListEntry unc_decoder_legacybase::buildChannelLis
entry.chroma_dst_plane[1] = img->get_channel_memory(paired_channel, &(entry.chroma_dst_plane_stride[1]));
entry.chroma_bytes_per_component_sample[1] = img->get_storage_bits_per_pixel(paired_channel) / 8;
+
+ // The coding of the two samples in the bitstream is taken from their 'uncC' components.
+ entry.chroma_bits_per_component_sample[0] = component.component_bit_depth;
+ entry.chroma_component_alignment[0] = component.component_align_size;
+
+ for (const Box_uncC::Component& other : m_uncC->get_components()) {
+ heif_channel other_channel;
+ if (map_uncompressed_component_to_channel(m_cmpd, other, &other_channel) &&
+ other_channel == paired_channel) {
+ entry.chroma_bits_per_component_sample[1] = other.component_bit_depth;
+ entry.chroma_component_alignment[1] = other.component_align_size;
+ break;
+ }
+ }
}
entry.bits_per_component_sample = component.component_bit_depth;
entry.component_alignment = component.component_align_size;
diff --git a/libheif/codecs/uncompressed/unc_decoder_legacybase.h b/libheif/codecs/uncompressed/unc_decoder_legacybase.h
index e2949f63..80593b90 100644
--- a/libheif/codecs/uncompressed/unc_decoder_legacybase.h
+++ b/libheif/codecs/uncompressed/unc_decoder_legacybase.h
@@ -175,6 +175,11 @@ protected:
uint8_t* chroma_dst_plane[2] = {nullptr, nullptr};
size_t chroma_dst_plane_stride[2] = {0, 0};
uint32_t chroma_bytes_per_component_sample[2] = {0, 0};
+ // How each of the two chroma samples is coded in the bitstream: the number of bits of
+ // the value and the component_align_size. This is not the same as the storage width
+ // above: a 12-bit sample is stored in two bytes, but coded with 12 bits.
+ uint16_t chroma_bits_per_component_sample[2] = {0, 0};
+ uint8_t chroma_component_alignment[2] = {0, 0};
uint16_t bits_per_component_sample;
uint8_t component_alignment;
uint32_t bytes_per_tile_row_src;
diff --git a/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc b/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
index b75a2318..5fbed329 100644
--- a/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
+++ b/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc
@@ -29,24 +29,67 @@
Result<std::vector<uint64_t>> unc_decoder_mixed_interleave::get_tile_data_sizes() const
{
uint64_t tile_size = 0;
+ bool chroma_counted = false;
for (const ChannelListEntry& entry : channelList) {
- uint32_t bits_per_component = entry.bits_per_component_sample;
- if (entry.channel != heif_channel_Cb && entry.channel != heif_channel_Cr
- && entry.component_alignment > 0) {
- uint32_t bytes_per_component = (bits_per_component + 7) / 8;
- skip_to_alignment(bytes_per_component, entry.component_alignment);
- bits_per_component = bytes_per_component * 8;
+ uint64_t bits_per_row;
+
+ if (entry.channel == heif_channel_Cb || entry.channel == heif_channel_Cr) {
+ // The two chroma components are stored together, like the components of a pixel in
+ // pixel interleave mode. They are counted once, at the first of the two.
+ if (chroma_counted) {
+ continue;
+ }
+ chroma_counted = true;
+
+ // Returns the bit position after one Cb/Cr pair that starts at bit position 'pos'.
+ // This mirrors how processTile() reads the pair: a byte-aligned component starts on a
+ // byte boundary and occupies component_align_size bytes, any other component
+ // occupies exactly its bit depth.
+ auto end_of_chroma_pair = [&entry](uint64_t pos) {
+ for (int c = 0; c < 2; c++) {
+ if (entry.chroma_component_alignment[c] != 0) {
+ pos = (pos + 7) & ~uint64_t{7};
+ pos += entry.chroma_component_alignment[c] * 8;
+ }
+ else {
+ pos += entry.chroma_bits_per_component_sample[c];
+ }
+ }
+ return pos;
+ };
+
+ // The first pair starts at the byte-aligned start of the row. All following pairs
+ // start at the same position within a byte as the second one, so they all have the
+ // size of the second pair.
+ uint64_t end_of_first_pair = end_of_chroma_pair(0);
+ uint64_t end_of_second_pair = end_of_chroma_pair(end_of_first_pair);
+
+ bits_per_row = 0;
+ if (entry.tile_width > 0) {
+ bits_per_row = end_of_first_pair + (end_of_second_pair - end_of_first_pair) * (entry.tile_width - 1);
+ }
+ }
+ else {
+ uint32_t bits_per_component = entry.bits_per_component_sample;
+ if (entry.component_alignment > 0) {
+ uint32_t bytes_per_component = (bits_per_component + 7) / 8;
+ skip_to_alignment(bytes_per_component, entry.component_alignment);
+ bits_per_component = bytes_per_component * 8;
+ }
+
+ bits_per_row = uint64_t{bits_per_component} * entry.tile_width;
}
- if (bits_per_component != 0 && entry.tile_width > UINT32_MAX / bits_per_component) {
+ if (bits_per_row > UINT32_MAX) {
return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size,
"uncompressed tile row size exceeds 32-bit range"};
}
- uint32_t bits_per_row = bits_per_component * entry.tile_width;
- bits_per_row = (bits_per_row + 7) & ~7U; // align to byte boundary
- tile_size += uint64_t{bits_per_row} / 8 * entry.tile_height;
+ uint32_t bytes_per_row = static_cast<uint32_t>((bits_per_row + 7) / 8); // rows end on a byte boundary
+ skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size());
+
+ tile_size += uint64_t{bytes_per_row} * entry.tile_height;
}
if (m_uncC->get_tile_align_size() != 0) {
@@ -87,7 +130,7 @@ void unc_decoder_mixed_interleave::processTile(UncompressedBitReader& srcBits, u
if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) {
if (!haveProcessedChromaForThisTile) {
for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) {
- // TODO: row padding
+ srcBits.markRowStart();
uint64_t dst_row_number = tile_y + channel_y0;
uint64_t chroma_dst_row_offset[2];
@@ -102,21 +145,37 @@ void unc_decoder_mixed_interleave::processTile(UncompressedBitReader& srcBits, u
// Each is written using its own plane's byte width -- Cb and Cr can be
// declared with different bit depths, so reusing one plane's width for
// the other overruns it (GHSA-x8r2-mggj-j6wr).
+ //
+ // A sample is coded with the bit depth of its component, as in pixel interleave
+ // mode, not with the width of the plane's storage word. Reading the full storage
+ // width instead put the bits of the neighbouring sample (or the alignment
+ // padding) into the upper bits of the value, so that a plane declared with, say,
+ // 12 bits received samples of up to 65535 (GHSA-v5rj-g4wv-j5w3). What such
+ // samples can do downstream is shown by GHSA-q7mw-2fmm-5q94.
for (int c = 0; c < 2; c++) {
+ if (entry.chroma_component_alignment[c] != 0) {
+ srcBits.skip_to_byte_boundary();
+ int numPadBits = (entry.chroma_component_alignment[c] * 8) - entry.chroma_bits_per_component_sample[c];
+ srcBits.skip_bits(numPadBits);
+ }
+
uint32_t bytes_per_sample = entry.chroma_bytes_per_component_sample[c];
uint64_t dst_column_offset = dst_column_number * bytes_per_sample;
- int val = srcBits.get_bits(bytes_per_sample * 8);
+ int val = srcBits.get_bits(entry.chroma_bits_per_component_sample[c]);
memcpy_to_native_endian(entry.chroma_dst_plane[c] + chroma_dst_row_offset[c] + dst_column_offset, val, bytes_per_sample);
}
}
+ srcBits.handleRowAlignment(m_uncC->get_row_align_size());
haveProcessedChromaForThisTile = true;
}
}
}
else {
for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) {
+ srcBits.markRowStart();
uint64_t dst_row_offset = (channel_y0 + tile_y) * entry.dst_plane_stride;
processComponentTileRow(entry, srcBits, dst_row_offset + channel_x0 * entry.bytes_per_component_sample);
+ srcBits.handleRowAlignment(m_uncC->get_row_align_size());
}
}
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a3bbf413..34a46e95 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -167,6 +167,7 @@ if (WITH_UNCOMPRESSED_CODEC)
add_libheif_test(uncompressed_sequence_odd_chroma)
add_libheif_test(uncompressed_sequence_tiles)
add_libheif_test(uncompressed_mixed_chroma_depth_overflow)
+ add_libheif_test(uncompressed_mixed_interleave_bit_depth)
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/uncompressed_mixed_interleave_bit_depth.cc b/tests/uncompressed_mixed_interleave_bit_depth.cc
new file mode 100644
index 00000000..2c8818a5
--- /dev/null
+++ b/tests/uncompressed_mixed_interleave_bit_depth.cc
@@ -0,0 +1,440 @@
+/*
+ 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.
+*/
+
+// In mixed interleave mode of 'unci' (ISO/IEC 23001-17, 5.2.1.6.4), the Cb and Cr samples
+// are stored like the components of a pixel in pixel interleave mode: each value is coded
+// with exactly component_bit_depth bits, or, if component_align_size is set, as a
+// byte-aligned word whose upper bits are padding.
+//
+// The decoder read each chroma sample with the width of the plane's storage word instead
+// (16 bits for a 12-bit component). The bits of the neighbouring sample, or the padding bits,
+// ended up in the upper bits of the value, so the decoded planes contained samples far above
+// the bit depth they declare (GHSA-v5rj-g4wv-j5w3). Code that relies on the bit depth
+// (libsharpyuv uses the samples as table indices) then read out of bounds. For well-formed
+// files with a chroma bit depth other than 8 or 16, the chroma planes were simply decoded
+// wrongly. The existing test files only use 8 and 16 bits, where the two widths coincide.
+//
+// These tests write the sample data with their own bit writer, following the specification,
+// and require that every sample is decoded exactly as it was written. Padding bits are
+// written as ones in one of the variants: the specification wants them to be zero, but a
+// decoder must not let them leak into the sample values.
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "test_utils.h"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace {
+
+struct Component
+{
+ uint16_t type; // 1 = Y, 2 = Cb, 3 = Cr
+ uint8_t bit_depth;
+ uint8_t align_size; // component_align_size
+};
+
+struct Layout
+{
+ std::string name;
+ uint32_t width;
+ uint32_t height;
+ uint8_t sampling_type; // 1 = 4:2:2, 2 = 4:2:0
+ std::vector<Component> components;
+ uint32_t row_align_size;
+};
+
+
+class TestBitWriter
+{
+public:
+ explicit TestBitWriter(bool padding_bit) : m_padding_bit(padding_bit) {}
+
+ void write_bits(uint32_t value, int n)
+ {
+ for (int i = n - 1; i >= 0; i--) {
+ write_bit((value >> i) & 1);
+ }
+ }
+
+ void write_padding_bits(int n)
+ {
+ for (int i = 0; i < n; i++) {
+ write_bit(m_padding_bit);
+ }
+ }
+
+ void pad_to_byte_boundary()
+ {
+ while (m_bits_in_last_byte != 0) {
+ write_bit(m_padding_bit);
+ }
+ }
+
+ void pad_row(size_t row_start, uint32_t row_align_size)
+ {
+ pad_to_byte_boundary();
+ if (row_align_size != 0) {
+ while ((m_data.size() - row_start) % row_align_size != 0) {
+ m_data.push_back(m_padding_bit ? 0xFF : 0x00);
+ }
+ }
+ }
+
+ // Only valid on a byte boundary.
+ size_t size() const { return m_data.size(); }
+
+ const std::vector<uint8_t>& data() const { return m_data; }
+
+private:
+ void write_bit(uint32_t bit)
+ {
+ if (m_bits_in_last_byte == 0) {
+ m_data.push_back(0);
+ }
+ if (bit) {
+ m_data.back() = static_cast<uint8_t>(m_data.back() | (0x80 >> m_bits_in_last_byte));
+ }
+ m_bits_in_last_byte = (m_bits_in_last_byte + 1) % 8;
+ }
+
+ std::vector<uint8_t> m_data;
+ int m_bits_in_last_byte = 0;
+ bool m_padding_bit;
+};
+
+
+// The value that is stored for a sample. It uses the whole range of the bit depth, including
+// the largest value, so that a bit that is taken from the wrong place shows up.
+uint32_t sample_value(const Component& c, uint32_t x, uint32_t y)
+{
+ uint32_t max_value = (1u << c.bit_depth) - 1;
+ uint32_t v = (x * 37 + y * 101 + c.type * 211) * 2654435761u;
+ if ((x + y) % 3 == 0) {
+ return max_value;
+ }
+ return (v >> 7) & max_value;
+}
+
+
+bool is_chroma(const Component& c) { return c.type == 2 || c.type == 3; }
+
+
+void write_component_value(TestBitWriter& writer, const Component& c, uint32_t value)
+{
+ if (c.align_size != 0) {
+ writer.pad_to_byte_boundary();
+ writer.write_padding_bits(c.align_size * 8 - c.bit_depth);
+ }
+ writer.write_bits(value, c.bit_depth);
+}
+
+
+std::vector<uint8_t> build_sample_data(const Layout& layout, bool padding_bit)
+{
+ uint32_t chroma_width = layout.width / 2;
+ uint32_t chroma_height = (layout.sampling_type == 2) ? layout.height / 2 : layout.height;
+
+ TestBitWriter writer(padding_bit);
+ bool chroma_written = false;
+
+ for (size_t i = 0; i < layout.components.size(); i++) {
+ const Component& c = layout.components[i];
+
+ if (!is_chroma(c)) {
+ // as in component interleave mode
+ for (uint32_t y = 0; y < layout.height; y++) {
+ size_t row_start = writer.size();
+ for (uint32_t x = 0; x < layout.width; x++) {
+ write_component_value(writer, c, sample_value(c, x, y));
+ }
+ writer.pad_row(row_start, layout.row_align_size);
+ }
+ }
+ else if (!chroma_written) {
+ // The two chroma components are consecutive in the component list. They are stored
+ // as in pixel interleave mode, in the order in which they are declared.
+ const Component& c2 = layout.components[i + 1];
+ REQUIRE(is_chroma(c2));
+
+ for (uint32_t y = 0; y < chroma_height; y++) {
+ size_t row_start = writer.size();
+ for (uint32_t x = 0; x < chroma_width; x++) {
+ write_component_value(writer, c, sample_value(c, x, y));
+ write_component_value(writer, c2, sample_value(c2, x, y));
+ }
+ writer.pad_row(row_start, layout.row_align_size);
+ }
+ chroma_written = true;
+ }
+ }
+
+ return writer.data();
+}
+
+
+std::vector<uint8_t> build_file(const Layout& layout, bool padding_bit)
+{
+ 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);
+
+ std::vector<uint8_t> infe_payload;
+ put_u16_be(infe_payload, 1);
+ put_u16_be(infe_payload, 0);
+ append_fourcc(infe_payload, "unci");
+ append_cstr(infe_payload, "");
+ auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2);
+
+ std::vector<uint8_t> iinf_payload;
+ put_u16_be(iinf_payload, 1);
+ append(iinf_payload, infe);
+ auto iinf = make_box("iinf", iinf_payload, /*full=*/true);
+
+ std::vector<uint8_t> ispe_payload;
+ put_u32_be(ispe_payload, layout.width);
+ put_u32_be(ispe_payload, layout.height);
+ auto ispe = make_box("ispe", ispe_payload, /*full=*/true);
+
+ std::vector<uint8_t> cmpd_payload;
+ put_u32_be(cmpd_payload, static_cast<uint32_t>(layout.components.size()));
+ for (const Component& c : layout.components) {
+ put_u16_be(cmpd_payload, c.type);
+ }
+ auto cmpd = make_box("cmpd", cmpd_payload);
+
+ std::vector<uint8_t> uncC_payload;
+ put_u32_be(uncC_payload, 0); // profile
+ put_u32_be(uncC_payload, static_cast<uint32_t>(layout.components.size()));
+ for (uint16_t idx = 0; idx < layout.components.size(); idx++) {
+ put_u16_be(uncC_payload, idx); // component_index
+ uncC_payload.push_back(static_cast<uint8_t>(layout.components[idx].bit_depth - 1)); // component_bit_depth_minus_one
+ uncC_payload.push_back(0); // component_format (unsigned)
+ uncC_payload.push_back(layout.components[idx].align_size); // component_align_size
+ }
+ uncC_payload.push_back(layout.sampling_type);
+ uncC_payload.push_back(2); // interleave_type = mixed
+ 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, layout.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
+ auto uncC = make_box("uncC", uncC_payload, /*full=*/true);
+
+ std::vector<uint8_t> ipco_payload;
+ append(ipco_payload, ispe);
+ append(ipco_payload, cmpd);
+ append(ipco_payload, uncC);
+ auto ipco = make_box("ipco", ipco_payload);
+
+ std::vector<uint8_t> ipma_payload;
+ put_u32_be(ipma_payload, 1); // entry_count
+ put_u16_be(ipma_payload, 1); // item_ID 1
+ ipma_payload.push_back(3); // association_count
+ ipma_payload.push_back(0x80 | 1); // essential, ispe
+ ipma_payload.push_back(0x80 | 2); // essential, cmpd
+ ipma_payload.push_back(0x80 | 3); // essential, uncC
+ 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);
+
+ // The item contains exactly the bytes of the image. If the decoder computed a larger
+ // size for the sample data than the layout has, it would refuse the file.
+ std::vector<uint8_t> sample_data = build_sample_data(layout, padding_bit);
+ auto idat = make_box("idat", sample_data);
+
+ 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, 1); // item_count
+ put_u16_be(iloc_payload, 1); // item_ID
+ 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, 0); // extent_offset (within idat)
+ put_u32_be(iloc_payload, static_cast<uint32_t>(sample_data.size())); // extent_length
+ 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, 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;
+}
+
+
+heif_channel channel_of(const Component& c)
+{
+ switch (c.type) {
+ case 2: return heif_channel_Cb;
+ case 3: return heif_channel_Cr;
+ default: return heif_channel_Y;
+ }
+}
+
+
+void check_decoding(const Layout& layout, bool padding_bit)
+{
+ INFO(layout.name << ", padding bits " << (padding_bit ? 1 : 0));
+
+ std::vector<uint8_t> file = build_file(layout, padding_bit);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ 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);
+
+ // Decode without any conversion, so that the planes come back as the decoder filled them.
+ heif_image* 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);
+ REQUIRE(img != nullptr);
+
+ uint32_t chroma_width = layout.width / 2;
+ uint32_t chroma_height = (layout.sampling_type == 2) ? layout.height / 2 : layout.height;
+
+ for (const Component& c : layout.components) {
+ heif_channel channel = channel_of(c);
+ uint32_t w = is_chroma(c) ? chroma_width : layout.width;
+ uint32_t h = is_chroma(c) ? chroma_height : layout.height;
+
+ REQUIRE(heif_image_get_bits_per_pixel_range(img, channel) == c.bit_depth);
+ REQUIRE(heif_image_get_width(img, channel) == static_cast<int>(w));
+ REQUIRE(heif_image_get_height(img, channel) == static_cast<int>(h));
+
+ size_t stride = 0;
+ const uint8_t* plane = heif_image_get_plane_readonly2(img, channel, &stride);
+ REQUIRE(plane != nullptr);
+
+ const uint32_t max_value = (1u << c.bit_depth) - 1;
+
+ for (uint32_t y = 0; y < h; y++) {
+ for (uint32_t x = 0; x < w; x++) {
+ uint32_t value;
+ if (c.bit_depth <= 8) {
+ value = plane[y * stride + x];
+ }
+ else {
+ value = reinterpret_cast<const uint16_t*>(plane + y * stride)[x];
+ }
+
+ INFO("component type " << c.type << " at (" << x << "," << y << ")");
+ REQUIRE(value <= max_value);
+ REQUIRE(value == sample_value(c, x, y));
+ }
+ }
+ }
+
+ heif_image_release(img);
+ heif_image_handle_release(handle);
+ heif_context_free(ctx);
+}
+
+} // namespace
+
+
+TEST_CASE("unci mixed interleave reads chroma samples with their component bit depth")
+{
+ const uint16_t Y = 1, Cb = 2, Cr = 3;
+
+ const std::vector<Layout> layouts = {
+ // The configuration of the report: all components 12 bit, 4:2:2, no alignment.
+ {"12 bit 4:2:2", 16, 8, 1, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 0},
+ {"12 bit 4:2:0", 16, 8, 2, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 0},
+ {"10 bit 4:2:0", 16, 8, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 0},
+
+ // Rows that do not end on a byte boundary: 6 luma samples of 10 bits are 60 bits,
+ // 3 chroma pairs of 2 x 10 bits are 60 bits.
+ {"10 bit 4:2:0, rows with padding to the byte boundary", 6, 4, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 0},
+ {"9 bit 4:2:2, rows with padding to the byte boundary", 6, 4, 1, {{Y, 9, 0}, {Cb, 9, 0}, {Cr, 9, 0}}, 0},
+
+ // Bit depths below 8 are stored in bytes and were read with 8 bits.
+ {"4 bit 4:2:2", 6, 4, 1, {{Y, 4, 0}, {Cb, 4, 0}, {Cr, 4, 0}}, 0},
+ {"8 bit luma, 5 bit chroma", 8, 4, 2, {{Y, 8, 0}, {Cb, 5, 0}, {Cr, 5, 0}}, 0},
+
+ // Different depths of the two chroma components, and Cr declared before Cb.
+ {"Cb 10 bit, Cr 12 bit", 8, 4, 2, {{Y, 8, 0}, {Cb, 10, 0}, {Cr, 12, 0}}, 0},
+ {"Cr 12 bit before Cb 10 bit", 8, 4, 2, {{Y, 8, 0}, {Cr, 12, 0}, {Cb, 10, 0}}, 0},
+ {"chroma before luma", 8, 4, 1, {{Cb, 12, 0}, {Cr, 12, 0}, {Y, 12, 0}}, 0},
+
+ // Byte-aligned components: each value is a 16-bit word with padding in the upper bits.
+ {"12 bit in 16-bit words", 8, 4, 2, {{Y, 12, 2}, {Cb, 12, 2}, {Cr, 12, 2}}, 0},
+ {"only Cb byte-aligned", 6, 4, 2, {{Y, 12, 0}, {Cb, 12, 2}, {Cr, 12, 0}}, 0},
+ {"only Cr byte-aligned", 6, 4, 2, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 2}}, 0},
+ {"5 bit in bytes", 6, 4, 1, {{Y, 5, 1}, {Cb, 5, 1}, {Cr, 5, 1}}, 0},
+
+ // Row alignment.
+ {"10 bit, rows aligned to 4 bytes", 6, 4, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 4},
+ {"12 bit, rows aligned to 8 bytes", 6, 4, 1, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 8},
+
+ // The depths at which the storage width and the bit depth coincide.
+ {"8 bit", 8, 4, 2, {{Y, 8, 0}, {Cb, 8, 0}, {Cr, 8, 0}}, 0},
+ {"16 bit", 8, 4, 1, {{Y, 16, 0}, {Cb, 16, 0}, {Cr, 16, 0}}, 0},
+ };
+
+ for (const Layout& layout : layouts) {
+ for (bool padding_bit : {false, true}) {
+ check_decoding(layout, padding_bit);
+ }
+ }
+}