Commit 3a1a8be7 for libheif

commit 3a1a8be758b266692b35b51d6f8ec9ab42b4c3fd
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Dec 13 23:57:40 2025 +0100

    [BSD3] write alpha channel track

diff --git a/examples/heif_enc.cc b/examples/heif_enc.cc
index 8b158f72..a25c9b33 100644
--- a/examples/heif_enc.cc
+++ b/examples/heif_enc.cc
@@ -2348,6 +2348,7 @@ int do_encode_sequence(heif_context* context, heif_encoder* encoder, heif_encodi
       encoding_options->gop_structure = sequence_gop_structure;
       encoding_options->keyframe_distance_min = sequence_keyframe_distance_min;
       encoding_options->keyframe_distance_max = sequence_keyframe_distance_max;
+      encoding_options->save_alpha_channel = master_alpha;

       image_width = static_cast<uint16_t>(w);
       image_height = static_cast<uint16_t>(h);
diff --git a/libheif/api/libheif/heif_sequences.cc b/libheif/api/libheif/heif_sequences.cc
index 433593ed..eb16d291 100644
--- a/libheif/api/libheif/heif_sequences.cc
+++ b/libheif/api/libheif/heif_sequences.cc
@@ -411,6 +411,7 @@ heif_sequence_encoding_options* heif_sequence_encoding_options_alloc()
   options->gop_structure = heif_sequence_gop_structure_lowdelay;
   options->keyframe_distance_min = 0;
   options->keyframe_distance_max = 0;
+  options->save_alpha_channel = 1;

   return options;
 }
diff --git a/libheif/api/libheif/heif_sequences.h b/libheif/api/libheif/heif_sequences.h
index 431893bf..84fa3583 100644
--- a/libheif/api/libheif/heif_sequences.h
+++ b/libheif/api/libheif/heif_sequences.h
@@ -312,6 +312,8 @@ void heif_track_options_set_timescale(heif_track_options*, uint32_t timescale);
  *
  * If 'false', all aux_info will be written as one block after the compressed image data.
  * This has the advantage that no aux_info offsets have to be written.
+ *
+ * Note: currently ignored. Interleaved writing is disabled.
  */
 LIBHEIF_API
 void heif_track_options_set_interleaved_sample_aux_infos(heif_track_options*, int interleaved_flag);
@@ -367,6 +369,8 @@ typedef struct heif_sequence_encoding_options
   enum heif_sequence_gop_structure gop_structure;
   int keyframe_distance_min; // 0 - undefined
   int keyframe_distance_max; // 0 - undefined
+
+  int save_alpha_channel;
 } heif_sequence_encoding_options;


diff --git a/libheif/context.cc b/libheif/context.cc
index 65b26f46..958abef8 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1404,7 +1404,7 @@ Result<std::shared_ptr<HeifPixelImage>> HeifContext::convert_to_output_colorspac
 }


-static Result<std::shared_ptr<HeifPixelImage>>
+Result<std::shared_ptr<HeifPixelImage>>
 create_alpha_image_from_image_alpha_channel(const std::shared_ptr<HeifPixelImage>& image,
                                             const heif_security_limits* limits)
 {
diff --git a/libheif/context.h b/libheif/context.h
index 8c19b0dd..ae3c8148 100644
--- a/libheif/context.h
+++ b/libheif/context.h
@@ -54,6 +54,11 @@ class Track;
 struct TrackOptions;


+Result<std::shared_ptr<HeifPixelImage>>
+create_alpha_image_from_image_alpha_channel(const std::shared_ptr<HeifPixelImage>& image,
+                                            const heif_security_limits* limits);
+
+
 // This is a higher-level view than HeifFile.
 // Images are grouped logically into main images and their thumbnails.
 // The class also handles automatic color-space conversion.
diff --git a/libheif/sequences/track.cc b/libheif/sequences/track.cc
index 57f59945..f415d0b8 100644
--- a/libheif/sequences/track.cc
+++ b/libheif/sequences/track.cc
@@ -92,6 +92,7 @@ void SampleAuxInfoHelper::add_nonpresent_sample()
 void SampleAuxInfoHelper::write_interleaved(const std::shared_ptr<HeifFile>& file)
 {
   if (m_interleaved && !m_data.empty()) {
+    // TODO: I think this does not work because the image data does not know that there is SAI in-between
     uint64_t pos = file->append_mdat_data(m_data);
     m_saio->add_sample_offset(pos);

@@ -601,7 +602,21 @@ heif_auxiliary_track_info_type Track::get_auxiliary_info_type() const
   }
 }

+const char* get_track_auxiliary_info_type(heif_compression_format format)
+{
+  switch (format) {
+    case heif_compression_HEVC:
+      return cAuxType_alpha_hevc;
+    case heif_compression_AVC:
+      return cAuxType_alpha_avc;
+    case heif_compression_AV1:
+      return cAuxType_alpha_miaf;
+    default:
+      return cAuxType_alpha_miaf; // TODO: is this correct for all remaining compression types ?
+  }
+}

+// TODO: is this correct or should we set the aux_info_type depending on the compression format?
 void Track::set_auxiliary_info_type(heif_auxiliary_track_info_type type)
 {
   switch (type) {
@@ -661,6 +676,28 @@ bool Track::end_of_sequence_reached() const

 Error Track::finalize_track()
 {
+  // --- write active chunk data
+
+  size_t data_start = m_heif_context->get_heif_file()->append_mdat_data(m_chunk_data);
+  m_chunk_data.clear();
+
+  // first sample in chunk? -> write chunk offset
+
+  if (true) { // m_stsc->last_chunk_empty()) {
+    // TODO: we will have to call this at the end of a chunk to dump the current SAI queue
+
+    // if auxiliary data is interleaved, write it between the chunks
+    if (m_aux_helper_tai_timestamps) m_aux_helper_tai_timestamps->write_interleaved(get_file());
+    if (m_aux_helper_content_ids) m_aux_helper_content_ids->write_interleaved(get_file());
+
+    // TODO
+    assert(data_start < 0xFF000000); // add some headroom for header data
+    m_stco->add_chunk_offset(static_cast<uint32_t>(data_start));
+  }
+
+
+  // --- write rest of data
+
   if (m_aux_helper_tai_timestamps) m_aux_helper_tai_timestamps->write_all(m_stbl, get_file());
   if (m_aux_helper_content_ids) m_aux_helper_content_ids->write_all(m_stbl, get_file());

@@ -742,19 +779,7 @@ void Track::set_sample_description_box(std::shared_ptr<Box> sample_description_b
 Error Track::write_sample_data(const std::vector<uint8_t>& raw_data, uint32_t sample_duration, bool is_sync_sample,
                                const heif_tai_timestamp_packet* tai, const std::optional<std::string>& gimi_contentID)
 {
-  size_t data_start = m_heif_context->get_heif_file()->append_mdat_data(raw_data);
-
-  // first sample in chunk? -> write chunk offset
-
-  if (m_stsc->last_chunk_empty()) {
-    // if auxiliary data is interleaved, write it between the chunks
-    if (m_aux_helper_tai_timestamps) m_aux_helper_tai_timestamps->write_interleaved(get_file());
-    if (m_aux_helper_content_ids) m_aux_helper_content_ids->write_interleaved(get_file());
-
-    // TODO
-    assert(data_start < 0xFF000000); // add some headroom for header data
-    m_stco->add_chunk_offset(static_cast<uint32_t>(data_start));
-  }
+  m_chunk_data.insert(m_chunk_data.end(), raw_data.begin(), raw_data.end());

   m_stsc->increase_samples_in_chunk(1);

@@ -849,7 +874,8 @@ void Track::init_sample_timing_table()
     timing.sample_duration_media_time = m_stts->get_sample_duration(i);
     current_decoding_time += timing.sample_duration_media_time;

-    while (i > m_chunks[current_chunk]->last_sample_number()) {
+    while (current_chunk < m_chunks.size() &&
+           i > m_chunks[current_chunk]->last_sample_number()) {
       current_chunk++;

       if (current_chunk > m_chunks.size()) {
diff --git a/libheif/sequences/track.h b/libheif/sequences/track.h
index 081aa8af..b445e66a 100644
--- a/libheif/sequences/track.h
+++ b/libheif/sequences/track.h
@@ -119,6 +119,9 @@ struct TrackOptions
 };


+const char* get_track_auxiliary_info_type(heif_compression_format format);
+
+
 class Track : public ErrorBuffer {
 public:
   //Track(HeifContext* ctx);
@@ -220,6 +223,7 @@ protected:
   void init_sample_timing_table();

   std::vector<std::shared_ptr<Chunk>> m_chunks;
+  std::vector<uint8_t> m_chunk_data;

   std::shared_ptr<Box_moov> m_moov;
   std::shared_ptr<Box_trak> m_trak;
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 3134228d..53b056a2 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -19,6 +19,8 @@
  */

 #include "track_visual.h"
+
+#include <memory>
 #include "codecs/decoder.h"
 #include "codecs/encoder.h"
 #include "chunk.h"
@@ -321,6 +323,16 @@ Error Track_Visual::encode_end_of_sequence(heif_encoder* h_encoder)
     }
   }

+
+  // --- also end alpha track
+
+  if (m_aux_alpha_track) {
+    auto err = m_aux_alpha_track->encode_end_of_sequence(m_alpha_track_encoder.get());
+    if (err) {
+      return err;
+    }
+  }
+
   return {};
 }

@@ -339,6 +351,50 @@ Error Track_Visual::encode_image(std::shared_ptr<HeifPixelImage> image,
     };
   }

+  m_image_class = input_class;
+
+
+  // --- If input has an alpha channel, add an alpha auxiliary track.
+
+  if (in_options->save_alpha_channel && image->has_alpha() && !m_aux_alpha_track) {
+    if (m_active_encoder) {
+      return {
+        heif_error_Usage_error,
+        heif_suberror_Unspecified,
+        "Input images must all either have an alpha channel or none of them."
+      };
+    }
+    else {
+      // alpha track uses default options, same timescale as color track
+
+      TrackOptions alphaOptions;
+      alphaOptions.track_timescale = m_track_info.track_timescale;
+
+      auto newAlphaTrackResult = m_heif_context->add_visual_sequence_track(&alphaOptions,
+                                                                           heif_track_type_auxiliary,
+                                                                           static_cast<uint16_t>(image->get_width()),
+                                                                           static_cast<uint16_t>(image->get_height()));
+
+      if (auto err = newAlphaTrackResult.error()) {
+        return err;
+      }
+
+      // add a reference to the color track
+
+      m_aux_alpha_track = *newAlphaTrackResult;
+      m_aux_alpha_track->add_reference_to_track(fourcc("auxl"), m_id);
+
+      // make a copy of the encoder from the color track for encoding the alpha track
+
+      m_alpha_track_encoder = std::make_unique<heif_encoder>(h_encoder->plugin);
+      heif_error err = m_alpha_track_encoder->alloc();
+      if (err.code) {
+        return {err.code, err.subcode, err.message};
+      }
+    }
+  }
+
+
   if (!m_active_encoder) {
     m_active_encoder = h_encoder;
   }
@@ -462,7 +518,32 @@ Error Track_Visual::encode_image(std::shared_ptr<HeifPixelImage> image,
   // --- get compressed data from encoder

   Result<bool> processingResult = process_encoded_data(h_encoder);
-  return processingResult.error();
+  if (auto err = processingResult.error()) {
+    return err;
+  }
+
+
+  // --- encode alpha channel into auxiliary track
+
+  if (m_aux_alpha_track) {
+    auto alphaImageResult = create_alpha_image_from_image_alpha_channel(colorConvertedImage,
+                                                                        m_heif_context->get_security_limits());
+    if (auto err = alphaImageResult.error()) {
+      return err;
+    }
+
+    (*alphaImageResult)->set_sample_duration(colorConvertedImage->get_sample_duration());
+
+    auto err = m_aux_alpha_track->encode_image(*alphaImageResult,
+                                               m_alpha_track_encoder.get(),
+                                               in_options,
+                                               heif_image_input_class_alpha);
+    if (err) {
+      return err;
+    }
+  }
+
+  return {};
 }


@@ -494,12 +575,19 @@ Result<bool> Track_Visual::process_encoded_data(heif_encoder* h_encoder)

       // add Coding-Constraints box (ccst) only if we are generating an image sequence

+      // TODO: does the alpha track also need a ccst box?
       if (m_hdlr->get_handler_type() == heif_track_type_image_sequence) {
         auto ccst = std::make_shared<Box_ccst>();
         ccst->set_coding_constraints(data.codingConstraints);
         sample_description_box->append_child_box(ccst);
       }

+      if (m_image_class == heif_image_input_class_alpha) {
+        auto auxi_box = std::make_shared<Box_auxi>();
+        auxi_box->set_aux_track_type_urn(get_track_auxiliary_info_type(h_encoder->plugin->compression_format));
+        sample_description_box->append_child_box(auxi_box);
+      }
+
       set_sample_description_box(sample_description_box);
       m_generated_sample_description_box = true;
     }
diff --git a/libheif/sequences/track_visual.h b/libheif/sequences/track_visual.h
index 0c40c32c..4eeab87f 100644
--- a/libheif/sequences/track_visual.h
+++ b/libheif/sequences/track_visual.h
@@ -65,6 +65,7 @@ public:
 private:
   uint16_t m_width = 0;
   uint16_t m_height = 0;
+  heif_image_input_class m_image_class;

   uintptr_t m_current_frame_nr = 0;
   bool m_generated_sample_description_box = false;
@@ -92,6 +93,7 @@ private:
   std::shared_ptr<Track_Visual> m_aux_alpha_track;

   heif_encoder* m_active_encoder = nullptr;
+  std::unique_ptr<heif_encoder> m_alpha_track_encoder;

   Result<bool> process_encoded_data(heif_encoder* encoder);
 };