Commit afea49b0 for libheif
commit afea49b047f04c43e772d91c7748e598a48a1248
Author: Dirk Lemstra <dirk@lemstra.org>
Date: Sun Oct 11 19:54:55 2026 +0200
Applied sample-entry color metadata to decoded sequence frames
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 258b507e..e97838fa 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -365,11 +365,22 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
// --- We have received a new decoded image.
// 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);
+
+ // Sample-entry colour information takes precedence over bitstream metadata.
+ for (const auto& colr : sampleDescription->get_child_boxes<Box_colr>()) {
+ image->set_color_profile(colr->get_color_profile());
+ }
+ if (auto clli = sampleDescription->get_child_box<Box_clli>()) {
+ image->set_clli(clli->clli);
+ }
+
if (m_stts) {
// 'sample_idx_in_chunk' follows m_next_sample_to_be_decoded, which runs ahead of the output
// by the decoder latency. Look the duration up by the output position instead, like
// Track::get_next_sample_raw_data() does.
- const SampleTiming& outputTiming = m_presentation_timeline[m_next_sample_to_be_output % m_presentation_timeline.size()];
image->set_sample_duration(m_stts->get_sample_duration(outputTiming.sampleIdx));
}
diff --git a/tests/image_description_metadata.cc b/tests/image_description_metadata.cc
index 51050652..7b32e0ed 100644
--- a/tests/image_description_metadata.cc
+++ b/tests/image_description_metadata.cc
@@ -1,6 +1,6 @@
/*
libheif unit tests for ImageDescription metadata preservation across
- HeifPixelImage transforms (rotation, crop, clone).
+ HeifPixelImage transforms (rotation, crop, clone) and sequence decoding.
MIT License
@@ -30,6 +30,9 @@
#include "image/image_description.h"
#include "libheif/heif.h"
#include "libheif/heif_uncompressed.h"
+#include "libheif/heif_sequences.h"
+#include "sequences/seq_boxes.h"
+#include <algorithm>
#include <cstring>
#include <memory>
@@ -283,3 +286,172 @@ TEST_CASE("create_clone_image_at_new_size preserves metadata and components",
check_metadata(clone, fix);
}
+
+
+static heif_error sequence_metadata_writer(heif_context*, const void* data, size_t size, void* userdata)
+{
+ auto& output = *static_cast<std::vector<uint8_t>*>(userdata);
+ const auto* bytes = static_cast<const uint8_t*>(data);
+ output.insert(output.end(), bytes, bytes + size);
+ return {heif_error_Ok, heif_suberror_Unspecified, nullptr};
+}
+
+static std::vector<uint8_t> encode_metadata_sequence()
+{
+ heif_context* context = heif_context_alloc();
+ heif_encoder* encoder = nullptr;
+ REQUIRE(heif_context_get_encoder_for_format(context, heif_compression_AV1, &encoder).code == heif_error_Ok);
+ heif_track* track = nullptr;
+ REQUIRE(heif_context_add_visual_sequence_track(context, 64, 64, heif_track_type_image_sequence,
+ nullptr, nullptr, &track).code == heif_error_Ok);
+ heif_image* image = nullptr;
+ REQUIRE(heif_image_create(64, 64, heif_colorspace_YCbCr, heif_chroma_444, &image).code == heif_error_Ok);
+ for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) {
+ REQUIRE(heif_image_add_plane(image, channel, 64, 64, 8).code == heif_error_Ok);
+ size_t stride = 0;
+ uint8_t* pixels = heif_image_get_plane2(image, channel, &stride);
+ REQUIRE(pixels != nullptr);
+ for (size_t y = 0; y < 64; ++y) {
+ std::fill(pixels + y * stride, pixels + y * stride + 64, channel == heif_channel_Y ? 64 : 128);
+ }
+ }
+ heif_image_set_duration(image, 1);
+ for (int frame = 0; frame < 3; ++frame) {
+ REQUIRE(heif_track_encode_sequence_image(track, image, encoder, nullptr).code == heif_error_Ok);
+ }
+ REQUIRE(heif_track_encode_end_of_sequence(track, encoder).code == heif_error_Ok);
+ std::vector<uint8_t> file;
+ heif_writer writer{};
+ writer.writer_api_version = 1;
+ writer.write = sequence_metadata_writer;
+ REQUIRE(heif_context_write(context, &writer, &file).code == heif_error_Ok);
+ heif_image_release(image);
+ heif_track_release(track);
+ heif_encoder_release(encoder);
+ heif_context_free(context);
+ return file;
+}
+
+static void add_sequence_metadata(std::vector<uint8_t>& file, const std::vector<uint8_t>& icc)
+{
+ auto reader = std::make_shared<StreamReader_memory>(file.data(), file.size(), false);
+ BitstreamRange range(reader, file.size());
+ while (reader->get_position() < file.size()) {
+ const size_t start = reader->get_position();
+ BoxHeader header;
+ REQUIRE(header.parse_header(range) == Error::Ok);
+ REQUIRE(header.get_box_size() >= header.get_header_size());
+ REQUIRE(header.get_box_size() <= file.size() - start);
+ if (header.get_short_type() != fourcc("moov")) {
+ REQUIRE(reader->seek(start + header.get_box_size()));
+ continue;
+ }
+ REQUIRE(reader->seek(start));
+ BitstreamRange movie_range(reader, header.get_box_size());
+ std::shared_ptr<Box> moov;
+ REQUIRE(Box::read(movie_range, &moov, heif_get_global_security_limits()) == Error::Ok);
+ auto trak = moov->get_child_box<Box_trak>();
+ REQUIRE(trak != nullptr);
+ auto mdia = trak->get_child_box<Box_mdia>();
+ REQUIRE(mdia != nullptr);
+ auto minf = mdia->get_child_box<Box_minf>();
+ REQUIRE(minf != nullptr);
+ auto stbl = minf->get_child_box<Box_stbl>();
+ REQUIRE(stbl != nullptr);
+ auto stsd = stbl->get_child_box<Box_stsd>();
+ REQUIRE(stsd != nullptr);
+ auto entry = std::const_pointer_cast<Box>(stsd->get_sample_entry(0));
+ REQUIRE(entry != nullptr);
+ nclx_profile profile;
+ profile.set_colour_primaries(9);
+ profile.set_transfer_characteristics(16);
+ profile.set_matrix_coefficients(9);
+ profile.set_full_range_flag(false);
+ auto colr = std::make_shared<Box_colr>();
+ colr->set_color_profile(std::make_shared<color_profile_nclx>(profile));
+ entry->replace_child_box(colr);
+ auto raw_colr = std::make_shared<Box_colr>();
+ raw_colr->set_color_profile(std::make_shared<color_profile_raw>(fourcc("prof"), icc));
+ entry->append_child_box(raw_colr);
+ auto clli = std::make_shared<Box_clli>();
+ clli->clli = {1000, 400};
+ entry->append_child_box(clli);
+ StreamWriter output;
+ REQUIRE(moov->write(output) == Error::Ok);
+ const auto data = output.get_data();
+ // Leave a same-sized free box in place and append the modified movie box,
+ // preserving the offsets of all encoded samples.
+ std::memcpy(file.data() + start + 4, "free", 4);
+ std::fill(file.begin() + start + header.get_header_size(),
+ file.begin() + start + header.get_box_size(), 0);
+ file.insert(file.end(), data.begin(), data.end());
+ return;
+ }
+ FAIL("Encoded sequence has no movie box");
+}
+
+TEST_CASE("sequence sample-entry colour metadata is applied to every decoded frame")
+{
+ if (!heif_have_encoder_for_format(heif_compression_AV1) ||
+ !heif_have_decoder_for_format(heif_compression_AV1)) {
+ SKIP("No AV1 encoder/decoder pair available");
+ }
+ const bool with_metadata = GENERATE(false, true);
+ const bool convert_to_rgb = GENERATE(false, true);
+ INFO("sample-entry metadata: " << with_metadata << ", RGB conversion: " << convert_to_rgb);
+ auto file = encode_metadata_sequence();
+ const std::vector<uint8_t> icc = {1, 2, 3, 4, 5};
+ if (with_metadata) {
+ add_sequence_metadata(file, icc);
+ }
+ heif_context* context = heif_context_alloc();
+ REQUIRE(heif_context_read_from_memory(context, file.data(), file.size(), nullptr).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;
+ for (int frame = 0; frame < 3; ++frame) {
+ INFO("frame: " << frame);
+ heif_image* image = nullptr;
+ REQUIRE(heif_track_decode_next_image(track, &image,
+ convert_to_rgb ? heif_colorspace_RGB : heif_colorspace_undefined,
+ convert_to_rgb ? heif_chroma_444 : heif_chroma_undefined,
+ options).code == heif_error_Ok);
+ REQUIRE(image != nullptr);
+ if (with_metadata && convert_to_rgb) {
+ size_t stride = 0;
+ const uint8_t* pixels = heif_image_get_plane_readonly2(image, heif_channel_R, &stride);
+ REQUIRE(pixels != nullptr);
+ CHECK(pixels[0] >= 55);
+ CHECK(pixels[0] <= 57);
+ }
+ if (with_metadata && !convert_to_rgb) {
+ heif_color_profile_nclx* nclx = nullptr;
+ REQUIRE(heif_image_get_nclx_color_profile(image, &nclx).code == heif_error_Ok);
+ CHECK(nclx->color_primaries == 9);
+ CHECK(nclx->transfer_characteristics == 16);
+ CHECK(nclx->matrix_coefficients == 9);
+ CHECK(nclx->full_range_flag == 0);
+ heif_nclx_color_profile_free(nclx);
+ }
+ CHECK(heif_image_get_raw_color_profile_size(image) == (with_metadata ? icc.size() : 0));
+ CHECK(heif_image_has_content_light_level(image) == (with_metadata ? 1 : 0));
+ if (with_metadata) {
+ std::vector<uint8_t> actual(icc.size());
+ REQUIRE(heif_image_get_raw_color_profile(image, actual.data()).code == heif_error_Ok);
+ 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);
+ }
+ 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);
+ heif_decoding_options_free(options);
+ heif_track_release(track);
+ heif_context_free(context);
+}