Commit 7126c9bc for libheif
commit 7126c9bc938cf7bc654c9c0ab34cf6383a804b37
Author: Dirk Lemstra <dirk@lemstra.org>
Date: Sun Oct 11 22:24:41 2026 +0200
Cache sample-description indexes for sequence chunks
diff --git a/libheif/sequences/chunk.h b/libheif/sequences/chunk.h
index 8b05fa14..8ee6667e 100644
--- a/libheif/sequences/chunk.h
+++ b/libheif/sequences/chunk.h
@@ -61,6 +61,10 @@ public:
uint32_t last_sample_number() const { return m_last_sample; }
+ uint32_t get_sample_description_index() const { return m_sample_description_index; }
+
+ void set_sample_description_index(uint32_t index) { m_sample_description_index = index; }
+
DataExtent get_data_extent_for_sample(uint32_t n) const;
void set_decoder(std::shared_ptr<class Decoder> dec) { m_decoder = std::move(dec); }
@@ -81,7 +85,7 @@ private:
uint32_t m_first_sample = 0;
uint32_t m_last_sample = 0;
- //uint32_t m_sample_description_index = 0;
+ uint32_t m_sample_description_index = 0;
uint32_t m_next_sample_to_be_decoded = 0;
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index 61102f9c..d2227cf8 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -427,6 +427,8 @@ Error Track::load(const std::shared_ptr<Box_trak>& trak_box)
};
}
+ chunk->set_sample_description_index(sampleToChunk.sample_description_index);
+
if (auto visualSampleDescription = std::dynamic_pointer_cast<const Box_VisualSampleEntry>(sample_description)) {
if (chunk_idx > 0 && (int32_t) sampleToChunk.sample_description_index == previous_sample_description_index) {
// reuse decoder from previous chunk if it uses the sample sample_description_index
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 6c0b10be..bd66b5a6 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -366,8 +366,8 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
// Postprocess decoded image, attach metadata.
const SampleTiming& outputTiming = m_presentation_timeline[m_next_sample_to_be_output % m_presentation_timeline.size()];
- const auto* sampleToChunk = m_stsc->get_chunk(outputTiming.chunkIdx + 1);
- auto sampleDescription = m_stsd->get_sample_entry(sampleToChunk->sample_description_index - 1);
+ const auto& chunk = m_chunks[outputTiming.chunkIdx];
+ auto sampleDescription = m_stsd->get_sample_entry(chunk->get_sample_description_index() - 1);
const auto bitstream_nclx = image->get_color_profile_nclx();
// Sample-entry colour information takes precedence over bitstream metadata.
diff --git a/tests/image_description_metadata.cc b/tests/image_description_metadata.cc
index 27f6f54c..17010213 100644
--- a/tests/image_description_metadata.cc
+++ b/tests/image_description_metadata.cc
@@ -323,6 +323,7 @@ static std::vector<uint8_t> encode_metadata_sequence()
heif_sequence_encoding_options* encoding_options = heif_sequence_encoding_options_alloc();
REQUIRE(encoding_options != nullptr);
encoding_options->output_nclx_profile = nclx;
+ encoding_options->gop_structure = heif_sequence_gop_structure_intra_only;
for (int frame = 0; frame < 3; ++frame) {
REQUIRE(heif_track_encode_sequence_image(track, image, encoder, encoding_options).code == heif_error_Ok);
}
@@ -341,7 +342,8 @@ static std::vector<uint8_t> encode_metadata_sequence()
return file;
}
-static void add_sequence_metadata(std::vector<uint8_t>& file, const std::vector<uint8_t>& icc)
+static void add_sequence_metadata(std::vector<uint8_t>& file, const std::vector<uint8_t>& icc,
+ bool multiple_descriptions = false)
{
auto reader = std::make_shared<StreamReader_memory>(file.data(), file.size(), false);
BitstreamRange range(reader, file.size());
@@ -385,6 +387,57 @@ static void add_sequence_metadata(std::vector<uint8_t>& file, const std::vector<
auto clli = std::make_shared<Box_clli>();
clli->clli = {1000, 400};
entry->append_child_box(clli);
+ if (multiple_descriptions) {
+ StreamWriter entry_writer;
+ REQUIRE(entry->write(entry_writer) == Error::Ok);
+ const auto entry_data = entry_writer.get_data();
+ auto entry_reader = std::make_shared<StreamReader_memory>(entry_data.data(), entry_data.size(), false);
+ BitstreamRange entry_range(entry_reader, entry_data.size());
+ std::shared_ptr<Box> second_entry;
+ REQUIRE(Box::read(entry_range, &second_entry, heif_get_global_security_limits()) == Error::Ok);
+ second_entry->get_child_box<Box_clli>()->clli = {2000, 800};
+ stsd->add_sample_entry(second_entry);
+
+ auto stco = stbl->get_child_box<Box_stco>();
+ auto stsz = stbl->get_child_box<Box_stsz>();
+ REQUIRE(stco != nullptr);
+ REQUIRE(stsz != nullptr);
+ REQUIRE(stco->get_offsets().size() == 1);
+ REQUIRE(stsz->num_samples() == 3);
+ StreamWriter tables;
+ tables.write32(40);
+ tables.write32(fourcc("stsc"));
+ tables.write32(0);
+ tables.write32(2);
+ for (uint32_t value : {1u, 1u, 1u, 2u, 2u, 2u}) {
+ tables.write32(value);
+ }
+ tables.write32(24);
+ tables.write32(fourcc("stco"));
+ tables.write32(0);
+ tables.write32(2);
+ tables.write32(stco->get_offsets()[0]);
+ const uint32_t first_sample_size = stsz->has_fixed_sample_size() ?
+ stsz->get_fixed_sample_size() : stsz->get_sample_sizes()[0];
+ tables.write32(stco->get_offsets()[0] + first_sample_size);
+ const auto table_data = tables.get_data();
+ auto table_reader = std::make_shared<StreamReader_memory>(table_data.data(), table_data.size(), false);
+ BitstreamRange table_range(table_reader, table_data.size());
+ for (int i = 0; i < 2; ++i) {
+ std::shared_ptr<Box> table;
+ REQUIRE(Box::read(table_range, &table, heif_get_global_security_limits()) == Error::Ok);
+ if (i == 0) {
+ auto mapping = std::dynamic_pointer_cast<Box_stsc>(table);
+ REQUIRE(mapping != nullptr);
+ REQUIRE(stbl->replace_child_box(mapping));
+ }
+ else {
+ auto offsets = std::dynamic_pointer_cast<Box_stco>(table);
+ REQUIRE(offsets != nullptr);
+ REQUIRE(stbl->replace_child_box(offsets));
+ }
+ }
+ }
StreamWriter output;
REQUIRE(moov->write(output) == Error::Ok);
const auto data = output.get_data();
@@ -408,22 +461,28 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
const bool with_metadata = GENERATE(false, true);
const bool convert_to_rgb = GENERATE(false, true);
const bool autocorrect = GENERATE(false, true);
+ const bool multiple_descriptions = GENERATE(false, true);
INFO("sample-entry metadata: " << with_metadata << ", RGB conversion: " << convert_to_rgb);
INFO("autocorrect: " << autocorrect);
auto file = encode_metadata_sequence();
const std::vector<uint8_t> icc = {1, 2, 3, 4, 5};
if (with_metadata) {
- add_sequence_metadata(file, icc);
+ add_sequence_metadata(file, icc, multiple_descriptions);
}
heif_context* context = heif_context_alloc();
- REQUIRE(heif_context_read_from_memory(context, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ const auto read_error = heif_context_read_from_memory(context, file.data(), file.size(), nullptr);
+ INFO(read_error.message);
+ REQUIRE(read_error.code == heif_error_Ok);
heif_track* track = heif_context_get_track(context, 0);
REQUIRE(track != nullptr);
heif_decoding_options* options = heif_decoding_options_alloc();
options->ignore_sequence_editlist = 1;
options->output_image_nclx_profile_passthrough = 1;
options->autocorrect_broken_input = autocorrect;
- for (int frame = 0; frame < 3; ++frame) {
+ // Check both descriptions across the chunk boundary. Final flushing with
+ // multiple decoder instances is not yet supported by the sequence decoder.
+ const int frames_to_check = with_metadata && multiple_descriptions ? 2 : 3;
+ for (int frame = 0; frame < frames_to_check; ++frame) {
INFO("frame: " << frame);
heif_image* image = nullptr;
REQUIRE(heif_track_decode_next_image(track, &image,
@@ -460,14 +519,16 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
CHECK(actual == icc);
heif_content_light_level clli{};
heif_image_get_content_light_level(image, &clli);
- CHECK(clli.max_content_light_level == 1000);
- CHECK(clli.max_pic_average_light_level == 400);
+ CHECK(clli.max_content_light_level == (multiple_descriptions && frame > 0 ? 2000 : 1000));
+ CHECK(clli.max_pic_average_light_level == (multiple_descriptions && frame > 0 ? 800 : 400));
}
heif_image_release(image);
}
- heif_image* image = nullptr;
- CHECK(heif_track_decode_next_image(track, &image, heif_colorspace_undefined,
- heif_chroma_undefined, options).code == heif_error_End_of_sequence);
+ if (!(with_metadata && multiple_descriptions)) {
+ heif_image* image = nullptr;
+ CHECK(heif_track_decode_next_image(track, &image, heif_colorspace_undefined,
+ heif_chroma_undefined, options).code == heif_error_End_of_sequence);
+ }
heif_decoding_options_free(options);
heif_track_release(track);
heif_context_free(context);