Commit 41bad534 for libheif

commit 41bad534c8d316c08507fcb26480d1c76223c802
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Tue Sep 29 21:23:05 2026 +0200

    Write the constraint flags of the HEVC profile into hvcC

    The 48 constraint flags in the 'hvcC' box of an encoded image were always
    zero, for two reasons. parse_sps_for_hvcC_configuration() skipped them in the
    SPS, and HEVCDecoderConfigurationRecord::write() shifted each byte once too
    often, which dropped the first flag of the byte and moved the others.

    ISO/IEC 14496-15 demands that the values in 'hvcC' match those of the
    parameter sets. The flags carry general_progressive_source_flag and
    general_frame_only_constraint_flag, and within the range extensions and the
    screen content coding extensions they are what tells the profiles apart.

    This changes the 'hvcC' box that libheif writes for all HEVC encoders.

diff --git a/libheif/codecs/hevc_boxes.cc b/libheif/codecs/hevc_boxes.cc
index 1658ac15..848b6188 100644
--- a/libheif/codecs/hevc_boxes.cc
+++ b/libheif/codecs/hevc_boxes.cc
@@ -123,11 +123,11 @@ Error HEVCDecoderConfigurationRecord::write(StreamWriter& writer) const
     uint8_t byte = 0;

     for (int b = 0; b < 8; b++) {
+      byte = (uint8_t) (byte << 1);
+
       if (general_constraint_indicator_flags[i * 8 + b]) {
         byte |= 1;
       }
-
-      byte = (uint8_t) (byte << 1);
     }

     writer.write8(byte);
@@ -625,9 +625,11 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
   config->general_profile_idc = reader.get_bits8(5);
   config->general_profile_compatibility_flags = reader.get_bits32(32);

-  reader.skip_bits(16); // skip reserved bits
-  reader.skip_bits(16); // skip reserved bits
-  reader.skip_bits(16); // skip reserved bits
+  // general_progressive_source_flag up to the last of the constraint flags. Within the range
+  // extensions and the screen content coding extensions, they tell the profiles apart.
+  for (int i = 0; i < HEVCDecoderConfigurationRecord::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) {
+    config->general_constraint_indicator_flags[i] = (reader.get_bits(1) != 0);
+  }

   config->general_level_idc = reader.get_bits8(8);

diff --git a/tests/hevc_sps.cc b/tests/hevc_sps.cc
index c208e745..20b6d74a 100644
--- a/tests/hevc_sps.cc
+++ b/tests/hevc_sps.cc
@@ -27,8 +27,11 @@
 #include "catch_amalgamated.hpp"
 #include "codecs/hevc_boxes.h"
 #include "codecs/decoder.h"
+#include "bitstream.h"
 #include "error.h"
+#include "libheif/heif.h"
 #include <cstdint>
+#include <memory>
 #include <vector>

 // SPS NAL unit (with emulation prevention bytes) taken from the hvcC box of
@@ -62,6 +65,56 @@ TEST_CASE("SPS conformance window yields visible and coded size")
 }


+TEST_CASE("SPS constraint flags are copied to hvcC")
+{
+  HEVCDecoderConfigurationRecord config{};
+  uint32_t width = 0, height = 0;
+  ImageSize coded{};
+
+  Error err = parse_sps_for_hvcC_configuration(rainbow_sps.data(), rainbow_sps.size(),
+                                               &config, &width, &height, &coded);
+  REQUIRE(!err);
+
+  // The SPS has general_progressive_source_flag and general_frame_only_constraint_flag
+  // set: the constraint flags are 0x90 and five zero bytes.
+
+  CHECK(config.general_profile_idc == HEVCDecoderConfigurationRecord::Profile_MainStillPicture);
+
+  for (int i = 0; i < HEVCDecoderConfigurationRecord::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) {
+    INFO("constraint flag " << i);
+    CHECK(config.general_constraint_indicator_flags[i] == (i == 0 || i == 3));
+  }
+
+  // They are written in the same order, the first flag in the most significant bit.
+
+  config.configuration_version = 1;
+
+  // a flag in each of the bytes, among them the first and the last bit of a byte
+  config.general_constraint_indicator_flags[8] = true;
+  config.general_constraint_indicator_flags[23] = true;
+  config.general_constraint_indicator_flags[47] = true;
+
+  StreamWriter writer;
+  err = config.write(writer);
+  REQUIRE(!err);
+
+  const std::vector<uint8_t> data = writer.get_data();
+  REQUIRE(data.size() >= 12);
+
+  const std::vector<uint8_t> constraint_flags(data.begin() + 6, data.begin() + 12);
+  CHECK(constraint_flags == std::vector<uint8_t>{0x90, 0x80, 0x01, 0x00, 0x00, 0x01});
+
+  // ... and read back into the same flags.
+
+  BitstreamRange range(std::make_shared<StreamReader_memory>(data.data(), data.size(), false), data.size());
+
+  HEVCDecoderConfigurationRecord parsed{};
+  err = parsed.parse(range, heif_get_global_security_limits());
+  REQUIRE(!err);
+  CHECK(parsed.general_constraint_indicator_flags == config.general_constraint_indicator_flags);
+}
+
+
 TEST_CASE("SPS chroma_format_idc out of range is rejected")
 {
   // Same SPS with chroma_format_idc changed from 1 (uvlc '010') to 4