Commit 6f20de4b for libheif

commit 6f20de4b75bb350fdc54c97e429f386b257764db
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri Feb 27 09:56:15 2026 +0100

    extend heif-gen-bayer to generate video sequences

diff --git a/examples/heif_gen_bayer.cc b/examples/heif_gen_bayer.cc
index 73106afb..7238f28c 100644
--- a/examples/heif_gen_bayer.cc
+++ b/examples/heif_gen_bayer.cc
@@ -27,13 +27,18 @@
 #include <cassert>
 #include <cstdlib>
 #include <cstring>
+#include <filesystem>
 #include <getopt.h>
+#include <iomanip>
 #include <iostream>
 #include <memory>
+#include <regex>
+#include <sstream>
 #include <string>
 #include <vector>

 #include <libheif/heif.h>
+#include <libheif/heif_sequences.h>
 #include <libheif/heif_uncompressed.h>

 #include "heifio/decoder_png.h"
@@ -151,12 +156,71 @@ static const PatternDefinition* find_pattern(const char* name)
 }


+static std::vector<std::string> deflate_input_filenames(const std::string& filename_example)
+{
+  std::regex pattern(R"((.*\D)?(\d+)(\..+)$)");
+  std::smatch match;
+
+  if (!std::regex_match(filename_example, match, pattern)) {
+    return {filename_example};
+  }
+
+  std::string prefix = match[1];
+
+  auto p = std::filesystem::absolute(std::filesystem::path(prefix));
+  std::filesystem::path directory = p.parent_path();
+  std::string filename_prefix = p.filename().string();
+  std::string number = match[2];
+  std::string suffix = match[3];
+
+  std::string patternString = filename_prefix + "(\\d+)" + suffix + "$";
+  pattern = patternString;
+
+  uint32_t digits = std::numeric_limits<uint32_t>::max();
+  uint32_t start = std::numeric_limits<uint32_t>::max();
+  uint32_t end = 0;
+
+  for (const auto& dirEntry : std::filesystem::directory_iterator(directory))
+  {
+    if (dirEntry.is_regular_file()) {
+      std::string s{dirEntry.path().filename().string()};
+
+      if (std::regex_match(s, match, pattern)) {
+        digits = std::min(digits, (uint32_t)match[1].length());
+
+        uint32_t number = std::stoi(match[1]);
+        start = std::min(start, number);
+        end = std::max(end, number);
+      }
+    }
+  }
+
+  std::vector<std::string> files;
+
+  for (uint32_t i = start; i <= end; i++)
+  {
+    std::stringstream sstr;
+
+    sstr << prefix << std::setw(digits) << std::setfill('0') << i << suffix;
+
+    std::filesystem::path p = directory / sstr.str();
+    files.emplace_back(p.string());
+  }
+
+  return files;
+}
+
+
 static void print_usage()
 {
-  std::cerr << "Usage: heif-gen-bayer [options] <input.png> <output.heif>\n\n"
+  std::cerr << "Usage: heif-gen-bayer [options] <input.png> <output.heif>\n"
+            << "       heif-gen-bayer -S [options] <frame_NNN.png> <output.mp4>\n\n"
             << "Options:\n"
             << "  -h, --help              show this help\n"
-            << "  -p, --pattern <name>    filter array pattern (default: rggb)\n\n"
+            << "  -p, --pattern <name>    filter array pattern (default: rggb)\n"
+            << "  -S, --sequence          sequence mode (expand numbered PNGs)\n"
+            << "  -V, --video             use video track handler (vide) instead of pict\n"
+            << "      --fps <N>           frames per second (default: 30)\n\n"
             << "Patterns:\n";
   for (int i = 0; i < num_patterns; i++) {
     std::cerr << "  " << patterns[i].name
@@ -168,57 +232,27 @@ static void print_usage()


 static struct option long_options[] = {
-    {(char* const) "help",    no_argument,       nullptr, 'h'},
-    {(char* const) "pattern", required_argument, nullptr, 'p'},
+    {(char* const) "help",     no_argument,       nullptr, 'h'},
+    {(char* const) "pattern",  required_argument, nullptr, 'p'},
+    {(char* const) "sequence", no_argument,       nullptr, 'S'},
+    {(char* const) "video",    no_argument,       nullptr, 'V'},
+    {(char* const) "fps",      required_argument, nullptr, 'f'},
     {nullptr, 0, nullptr, 0}
 };


-int main(int argc, char* argv[])
+// Create a bayer image from a PNG file. Returns nullptr on error.
+// If expected_width/expected_height are non-zero, the PNG must match those dimensions.
+static heif_image* create_bayer_image_from_png(const char* png_filename,
+                                               const PatternDefinition* pat,
+                                               int expected_width,
+                                               int expected_height)
 {
-  const PatternDefinition* pat = &patterns[0]; // default: RGGB
-
-  while (true) {
-    int option_index = 0;
-    int c = getopt_long(argc, argv, "hp:", long_options, &option_index);
-    if (c == -1)
-      break;
-
-    switch (c) {
-      case 'h':
-        print_usage();
-        return 0;
-
-      case 'p':
-        pat = find_pattern(optarg);
-        if (!pat) {
-          std::cerr << "Unknown pattern: " << optarg << "\n";
-          print_usage();
-          return 1;
-        }
-        break;
-
-      default:
-        print_usage();
-        return 1;
-    }
-  }
-
-  if (argc - optind != 2) {
-    print_usage();
-    return 1;
-  }
-
-  const char* input_filename = argv[optind];
-  const char* output_filename = argv[optind + 1];
-
-  // --- Load PNG
-
   InputImage input_image;
-  heif_error err = loadPNG(input_filename, 8, &input_image);
+  heif_error err = loadPNG(png_filename, 8, &input_image);
   if (err.code != heif_error_Ok) {
-    std::cerr << "Cannot load PNG: " << err.message << "\n";
-    return 1;
+    std::cerr << "Cannot load PNG '" << png_filename << "': " << err.message << "\n";
+    return nullptr;
   }

   heif_image* src_img = input_image.image.get();
@@ -229,27 +263,31 @@ int main(int argc, char* argv[])
   int bpp = heif_image_get_bits_per_pixel_range(src_img, heif_channel_interleaved);
   if (bpp != 8) {
     std::cerr << "Only 8-bit PNG input is supported. Got " << bpp << " bits per pixel.\n";
-    return 1;
+    return nullptr;
+  }
+
+  if (expected_width != 0 && (width != expected_width || height != expected_height)) {
+    std::cerr << "Frame '" << png_filename << "' has dimensions " << width << "x" << height
+              << " but expected " << expected_width << "x" << expected_height << "\n";
+    return nullptr;
   }

   if (width % pat->width != 0 || height % pat->height != 0) {
     std::cerr << "Image dimensions must be multiples of the pattern size ("
               << pat->width << "x" << pat->height << "). Got "
               << width << "x" << height << "\n";
-    return 1;
+    return nullptr;
   }

-  // --- Get source RGB data
-
+  // Get source RGB data
   int src_stride;
   const uint8_t* src_data = heif_image_get_plane_readonly(src_img, heif_channel_interleaved, &src_stride);
   if (!src_data) {
     std::cerr << "Failed to get interleaved RGB plane from PNG.\n";
-    return 1;
+    return nullptr;
   }

-  // --- Create Bayer image
-
+  // Create Bayer image
   heif_image* bayer_img = nullptr;
   err = heif_image_create(width, height,
                           heif_colorspace_filter_array,
@@ -257,21 +295,20 @@ int main(int argc, char* argv[])
                           &bayer_img);
   if (err.code != heif_error_Ok) {
     std::cerr << "Cannot create image: " << err.message << "\n";
-    return 1;
+    return nullptr;
   }

   err = heif_image_add_plane(bayer_img, heif_channel_filter_array, width, height, 8);
   if (err.code != heif_error_Ok) {
     std::cerr << "Cannot add plane: " << err.message << "\n";
     heif_image_release(bayer_img);
-    return 1;
+    return nullptr;
   }

   int dst_stride;
   uint8_t* dst_data = heif_image_get_plane(bayer_img, heif_channel_filter_array, &dst_stride);

-  // --- Convert RGB to filter array using the selected pattern
-
+  // Convert RGB to filter array using the selected pattern
   for (int y = 0; y < height; y++) {
     const uint8_t* src_row = src_data + y * src_stride;
     uint8_t* dst_row = dst_data + y * dst_stride;
@@ -289,21 +326,204 @@ int main(int argc, char* argv[])
         case heif_uncompressed_component_type_red:   dst_row[x] = r; break;
         case heif_uncompressed_component_type_green: dst_row[x] = g; break;
         case heif_uncompressed_component_type_blue:  dst_row[x] = b; break;
-        case heif_uncompressed_component_type_Y: dst_row[x] = static_cast<uint8_t>((r + g + b) / 3); break; // Y / white
+        case heif_uncompressed_component_type_Y: dst_row[x] = static_cast<uint8_t>((r + g + b) / 3); break;
         default:
           assert(false);
       }
     }
   }

-  // --- Set Bayer pattern metadata
-
+  // Set Bayer pattern metadata
   err = heif_image_set_bayer_pattern(bayer_img,
                                      pat->width, pat->height,
                                      pat->cpat.data());
   if (err.code != heif_error_Ok) {
     std::cerr << "Cannot set Bayer pattern: " << err.message << "\n";
     heif_image_release(bayer_img);
+    return nullptr;
+  }
+
+  return bayer_img;
+}
+
+
+static int encode_sequence(const std::vector<std::string>& filenames,
+                           const PatternDefinition* pat,
+                           int fps,
+                           bool use_video_handler,
+                           const char* output_filename)
+{
+  heif_context* ctx = heif_context_alloc();
+
+  heif_encoder* encoder = nullptr;
+  heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+  if (err.code != heif_error_Ok) {
+    std::cerr << "Cannot get uncompressed encoder: " << err.message << "\n";
+    heif_context_free(ctx);
+    return 1;
+  }
+
+  heif_context_set_sequence_timescale(ctx, fps);
+
+  heif_sequence_encoding_options* enc_options = heif_sequence_encoding_options_alloc();
+  heif_track* track = nullptr;
+  int first_width = 0, first_height = 0;
+
+  for (size_t i = 0; i < filenames.size(); i++) {
+    heif_image* bayer_img = create_bayer_image_from_png(filenames[i].c_str(), pat,
+                                                        first_width, first_height);
+    if (!bayer_img) {
+      heif_sequence_encoding_options_release(enc_options);
+      if (track) heif_track_release(track);
+      heif_encoder_release(encoder);
+      heif_context_free(ctx);
+      return 1;
+    }
+
+    if (i == 0) {
+      first_width = heif_image_get_primary_width(bayer_img);
+      first_height = heif_image_get_primary_height(bayer_img);
+
+      heif_track_type track_type = use_video_handler ? heif_track_type_video
+                                                     : heif_track_type_image_sequence;
+
+      heif_track_options* track_options = heif_track_options_alloc();
+      heif_track_options_set_timescale(track_options, fps);
+
+      err = heif_context_add_visual_sequence_track(ctx,
+                                                   static_cast<uint16_t>(first_width),
+                                                   static_cast<uint16_t>(first_height),
+                                                   track_type,
+                                                   track_options,
+                                                   enc_options,
+                                                   &track);
+      heif_track_options_release(track_options);
+
+      if (err.code != heif_error_Ok) {
+        std::cerr << "Cannot create sequence track: " << err.message << "\n";
+        heif_image_release(bayer_img);
+        heif_sequence_encoding_options_release(enc_options);
+        heif_encoder_release(encoder);
+        heif_context_free(ctx);
+        return 1;
+      }
+    }
+
+    heif_image_set_duration(bayer_img, 1);
+
+    err = heif_track_encode_sequence_image(track, bayer_img, encoder, enc_options);
+    heif_image_release(bayer_img);
+
+    if (err.code != heif_error_Ok) {
+      std::cerr << "Cannot encode frame " << i << ": " << err.message << "\n";
+      heif_sequence_encoding_options_release(enc_options);
+      heif_track_release(track);
+      heif_encoder_release(encoder);
+      heif_context_free(ctx);
+      return 1;
+    }
+
+    std::cout << "Encoded frame " << (i + 1) << "/" << filenames.size()
+              << ": " << filenames[i] << "\n";
+  }
+
+  err = heif_track_encode_end_of_sequence(track, encoder);
+  if (err.code != heif_error_Ok) {
+    std::cerr << "Cannot end sequence: " << err.message << "\n";
+  }
+
+  heif_sequence_encoding_options_release(enc_options);
+  heif_track_release(track);
+  heif_encoder_release(encoder);
+
+  err = heif_context_write_to_file(ctx, output_filename);
+  if (err.code != heif_error_Ok) {
+    std::cerr << "Cannot write file: " << err.message << "\n";
+    heif_context_free(ctx);
+    return 1;
+  }
+
+  heif_context_free(ctx);
+
+  std::cout << "Wrote " << filenames.size() << " frame(s) to " << output_filename << "\n";
+  return 0;
+}
+
+
+int main(int argc, char* argv[])
+{
+  const PatternDefinition* pat = &patterns[0]; // default: RGGB
+  bool sequence_mode = false;
+  bool use_video_handler = false;
+  int fps = 30;
+
+  while (true) {
+    int option_index = 0;
+    int c = getopt_long(argc, argv, "hp:SV", long_options, &option_index);
+    if (c == -1)
+      break;
+
+    switch (c) {
+      case 'h':
+        print_usage();
+        return 0;
+
+      case 'p':
+        pat = find_pattern(optarg);
+        if (!pat) {
+          std::cerr << "Unknown pattern: " << optarg << "\n";
+          print_usage();
+          return 1;
+        }
+        break;
+
+      case 'S':
+        sequence_mode = true;
+        break;
+
+      case 'V':
+        use_video_handler = true;
+        break;
+
+      case 'f': // --fps
+        fps = std::atoi(optarg);
+        if (fps <= 0) {
+          std::cerr << "Invalid FPS value: " << optarg << "\n";
+          return 1;
+        }
+        break;
+
+      default:
+        print_usage();
+        return 1;
+    }
+  }
+
+  if (argc - optind != 2) {
+    print_usage();
+    return 1;
+  }
+
+  const char* input_filename = argv[optind];
+  const char* output_filename = argv[optind + 1];
+
+  if (sequence_mode) {
+    // --- Sequence mode: expand numbered filenames and encode as sequence
+
+    std::vector<std::string> filenames = deflate_input_filenames(input_filename);
+    if (filenames.empty()) {
+      std::cerr << "No input files found matching pattern: " << input_filename << "\n";
+      return 1;
+    }
+
+    std::cout << "Found " << filenames.size() << " frame(s), encoding at " << fps << " fps\n";
+    return encode_sequence(filenames, pat, fps, use_video_handler, output_filename);
+  }
+
+  // --- Single image mode
+
+  heif_image* bayer_img = create_bayer_image_from_png(input_filename, pat, 0, 0);
+  if (!bayer_img) {
     return 1;
   }

@@ -312,7 +532,7 @@ int main(int argc, char* argv[])
   heif_context* ctx = heif_context_alloc();

   heif_encoder* encoder = nullptr;
-  err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
+  heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder);
   if (err.code != heif_error_Ok) {
     std::cerr << "Cannot get uncompressed encoder: " << err.message << "\n";
     heif_image_release(bayer_img);