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);