Commit 45a4563f for libheif

commit 45a4563f32aede17862431373c6f3e9c5621125c
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Feb 26 21:04:22 2026 +0100

    heif-enc: extract VMT code into separate file (#1706)

diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index d5cee58e..fc7f2258 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -74,6 +74,10 @@ if (WITH_HEADER_COMPRESSION)
     target_compile_definitions(heif-enc PRIVATE WITH_HEADER_COMPRESSION=1)
 endif ()

+if (ENABLE_EXPERIMENTAL_FEATURES)
+    target_sources(heif-enc PRIVATE vmt.cc vmt.h)
+endif ()
+

 if (BUILD_DEVELOPMENT_TOOLS AND PNG_FOUND)
     add_executable(heif-gen-bayer
diff --git a/examples/heif_enc.cc b/examples/heif_enc.cc
index 3e08501b..ab2d9b8b 100644
--- a/examples/heif_enc.cc
+++ b/examples/heif_enc.cc
@@ -58,6 +58,10 @@
 #include "libheif/heif_sequences.h"
 #include "libheif/heif_uncompressed.h"

+#if HEIF_ENABLE_EXPERIMENTAL_FEATURES
+#include "vmt.h"
+#endif
+
 // --- command line parameters

 int master_alpha = 1;
@@ -96,7 +100,6 @@ uint32_t sequence_repetitions = 1;
 std::string vmt_metadata_file;
 bool binary_metadata_track = false;
 std::string metadata_track_uri = "vmt:metadata";
-const uint32_t BAD_VMT_TIMESTAMP = 0xFFFFFFFE;

 int quality = 50;
 bool lossless = false;
@@ -2333,279 +2336,6 @@ std::vector<std::string> deflate_input_filenames(const std::string& filename_exa
 }


-std::optional<uint8_t> nibble_to_val(char c)
-{
-  if (c>='0' && c<='9') {
-    return c - '0';
-  }
-  if (c>='a' && c<='f') {
-    return c - 'a' + 10;
-  }
-  if (c>='A' && c<='F') {
-    return c - 'A' + 10;
-  }
-
-  return std::nullopt;
-}
-
-// Convert hex data to raw binary. Ignore any non-hex characters.
-static std::vector<uint8_t> hex_to_binary(const std::string& line)
-{
-  std::vector<uint8_t> data;
-  uint8_t current_value = 0;
-
-  bool high_nibble = true;
-  for (auto c : line) {
-    auto v = nibble_to_val(c);
-    if (v) {
-      if (high_nibble) {
-        current_value = static_cast<uint8_t>(*v << 4);
-        high_nibble = false;
-      }
-      else {
-        current_value |= *v;
-        data.push_back(current_value);
-        high_nibble = true;
-      }
-    }
-  }
-
-  return data;
-}
-
-
-// Convert base64 data to raw binary.
-static std::vector<uint8_t> decode_base64(const std::string& line)
-{
-  const std::string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-  std::vector<uint8_t> data;
-
-  size_t len = line.size();
-  if (len % 4 != 0) {
-    len = int(len / 4) * 4;
-  }
-
-  for (size_t i = 0; i < len; i += 4) {
-    uint8_t buf[4];
-    for (uint8_t j = 0; j < 4; j++) {
-      size_t k = base64.find(line[i + j]);
-      buf[j] = (uint8_t)(k == std::string::npos ? base64.size() : k);
-    }
-
-    data.push_back((uint8_t)(buf[0] << 2) + (buf[1] >> 4));
-
-    if (line[i + 2] != '=') {
-      data.push_back((uint8_t)((buf[1] & 0x0f) << 4) + (buf[2] >> 2));
-    }
-
-    if (line[i + 3] != '=') {
-      data.push_back((uint8_t)((buf[2] & 0x03) << 6) + buf[3]);
-    }
-  }
-
-  return data;
-}
-
-
-// Parse metadata from WebVMT sync commmands
-static std::vector<uint8_t> parse_vmt_sync_data(const std::string& content)
-{
-  std::vector<uint8_t> data;
-
-  std::regex pattern_sync(R"(\s*\{\s*\"sync\"\s*:\s*\{(.*?)\}\s*\}\s*)");
-  const std::sregex_token_iterator ti_end;
-
-  for (std::sregex_token_iterator ti(content.begin(), content.end(), pattern_sync, 1); ti != ti_end; ++ti)
-  {
-    std::string sync = *ti;
-    std::regex pattern_type(R"(.*\"type\"\s*:\s*\"(.*?)\".*)");
-    std::regex pattern_data(R"(.*\"data\"\s*:\s*\"(.*?)\".*)");
-    std::smatch match;
-
-    if (std::regex_match(sync, match, pattern_type)) {
-      std::string type = match[1];
-
-      std::regex pattern_hex(R"(.*\.hex$)");
-      std::regex pattern_b64(R"(.*\.base64$)");
-
-      std::string textData;
-      if (std::regex_match(sync, match, pattern_data)) {
-        textData = match[1];
-      }
-
-      if (std::regex_match(type, match, pattern_hex)) {
-        std::vector<uint8_t> binaryData = hex_to_binary(textData);
-        data.insert(data.end(), binaryData.begin(), binaryData.end());
-      }
-      else if (std::regex_match(type, match, pattern_b64)) {
-        std::vector<uint8_t> binaryData = decode_base64(textData);
-        data.insert(data.end(), binaryData.begin(), binaryData.end());
-      }
-      else {
-        data.insert(data.end(), textData.data(), textData.data() + textData.length());
-      }
-    }
-  }
-
-  return data;
-}
-
-
-uint32_t parse_vmt_timestamp(const std::string& vmt_time)
-{
-  std::regex pattern(R"(-?((\d*):)?(\d\d):(\d\d)(\.(\d*))?)");
-  std::smatch match;
-
-  if (!std::regex_match(vmt_time, match, pattern)) {
-    return 0; // no match
-  }
-
-  std::string hh = match[2]; // optional
-  std::string mm = match[3];
-  std::string ss = match[4];
-  std::string fs = match[6]; // optional
-
-  if (vmt_time.find('-') != std::string::npos) {
-    return 0; // negative time not supported
-  }
-
-  uint32_t ms = 0;
-
-  if (fs != "") {
-    if (fs.length() == 3) {
-      ms = std::stoi(fs);
-    }
-    else {
-      return BAD_VMT_TIMESTAMP; // invalid
-    }
-  }
-
-  uint32_t ts = ((hh != "" ? std::stoi(hh) : 0) * 3600 * 1000 +
-                 std::stoi(mm) * 60 * 1000 +
-                 std::stoi(ss) * 1000 +
-                 ms);
-
-  return ts;
-}
-
-
-int encode_vmt_metadata_track(heif_context* context, heif_track* visual_track,
-                              const std::string& track_uri, bool binary)
-{
-  // --- add metadata track
-
-  heif_track* track = nullptr;
-
-  heif_track_options* track_options = heif_track_options_alloc();
-  heif_track_options_set_timescale(track_options, 1000);
-
-  heif_context_add_uri_metadata_sequence_track(context, track_uri.c_str(), track_options, &track);
-  heif_raw_sequence_sample* sample = heif_raw_sequence_sample_alloc();
-
-
-  std::ifstream istr(vmt_metadata_file.c_str());
-
-  std::regex pattern_cue(R"(^\s*(-?(\d|:|\.)*)\s*-->\s*(-?(\d|:|\.)*)?.*)");
-  std::regex pattern_note(R"(^\s*(NOTE).*)");
-
-  static std::vector<uint8_t> prev_metadata;
-  static std::optional<uint32_t> prev_ts;
-
-  std::string line;
-  while (std::getline(istr, line))
-  {
-    std::smatch match;
-
-    if (std::regex_match(line, match, pattern_note)) {
-      while (std::getline(istr, line)) {
-        if (line.empty()) {
-          break;
-        }
-      }
-
-      continue;
-    }
-
-    if (!std::regex_match(line, match, pattern_cue)) {
-      continue;
-    }
-
-    std::string cue_start = match[1];
-    std::string cue_end = match[3]; // == "" for unbounded cues
-
-    uint32_t ts = parse_vmt_timestamp(cue_start);
-
-    std::vector<uint8_t> concat;
-
-    if (binary) {
-      while (std::getline(istr, line)) {
-        if (line.empty()) {
-          break;
-        }
-
-        std::vector<uint8_t> binaryData = hex_to_binary(line);
-        concat.insert(concat.end(), binaryData.begin(), binaryData.end());
-      }
-
-    }
-    else {
-      while (std::getline(istr, line)) {
-        if (line.empty()) {
-          break;
-        }
-
-        concat.insert(concat.end(), line.data(), line.data() + line.length());
-        concat.push_back('\n');
-      }
-
-      concat.push_back(0);
-      std::string content(concat.begin(), concat.end());
-      concat = parse_vmt_sync_data(content);
-    }
-
-    if (ts != BAD_VMT_TIMESTAMP) {
-
-      if (ts > *prev_ts) {
-        heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size());
-        heif_raw_sequence_sample_set_duration(sample, ts - *prev_ts);
-        heif_track_add_raw_sequence_sample(track, sample);
-      }
-      else if (ts == *prev_ts) {
-        concat.insert(concat.begin(), prev_metadata.begin(), prev_metadata.end());
-      }
-      else {
-        std::cerr << "Bad WebVMT timestamp order: " << cue_start << "\n";
-      }
-
-      prev_ts = ts;
-      prev_metadata = concat;
-    }
-    else {
-      std::cerr << "Bad WebVMT timestamp: " << cue_start << "\n";
-    }
-  }
-
-  // --- flush last metadata packet
-
-  heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size());
-  heif_raw_sequence_sample_set_duration(sample, 1);
-  heif_track_add_raw_sequence_sample(track, sample);
-
-  // --- add track reference
-
-  heif_track_add_reference_to_track(track, heif_track_reference_type_description, visual_track);
-
-  // --- release all objects
-
-  heif_raw_sequence_sample_release(sample);
-  heif_track_options_release(track_options);
-  heif_track_release(track);
-
-  return 0;
-}
-
-
-
 int do_encode_sequence(heif_context* context, heif_encoder* encoder, heif_encoding_options* options, std::vector<std::string> args)
 {
   if (args.size() == 1) {
@@ -2757,12 +2487,14 @@ int do_encode_sequence(heif_context* context, heif_encoder* encoder, heif_encodi
     return 5;
   }

+#if HEIF_ENABLE_EXPERIMENTAL_FEATURES
   if (!vmt_metadata_file.empty()) {
-    int ret = encode_vmt_metadata_track(context, track, metadata_track_uri, binary_metadata_track);
+    int ret = encode_vmt_metadata_track(context, track, vmt_metadata_file, metadata_track_uri, binary_metadata_track);
     if (ret) {
       return ret;
     }
   }
+#endif

   heif_track_release(track);
   heif_sequence_encoding_options_release(encoding_options);
diff --git a/examples/vmt.cc b/examples/vmt.cc
new file mode 100644
index 00000000..001200a1
--- /dev/null
+++ b/examples/vmt.cc
@@ -0,0 +1,313 @@
+/*
+  libheif example application "heif-enc" - VMT metadata track support.
+
+  MIT License
+
+  Copyright (c) 2017 Dirk Farin <dirk.farin@gmail.com>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#include "vmt.h"
+
+#include <cstdint>
+#include <fstream>
+#include <iostream>
+#include <optional>
+#include <regex>
+#include <string>
+#include <vector>
+
+#include <libheif/heif.h>
+#include <libheif/heif_sequences.h>
+
+
+static const uint32_t BAD_VMT_TIMESTAMP = 0xFFFFFFFE;
+
+
+static std::optional<uint8_t> nibble_to_val(char c)
+{
+  if (c>='0' && c<='9') {
+    return c - '0';
+  }
+  if (c>='a' && c<='f') {
+    return c - 'a' + 10;
+  }
+  if (c>='A' && c<='F') {
+    return c - 'A' + 10;
+  }
+
+  return std::nullopt;
+}
+
+// Convert hex data to raw binary. Ignore any non-hex characters.
+static std::vector<uint8_t> hex_to_binary(const std::string& line)
+{
+  std::vector<uint8_t> data;
+  uint8_t current_value = 0;
+
+  bool high_nibble = true;
+  for (auto c : line) {
+    auto v = nibble_to_val(c);
+    if (v) {
+      if (high_nibble) {
+        current_value = static_cast<uint8_t>(*v << 4);
+        high_nibble = false;
+      }
+      else {
+        current_value |= *v;
+        data.push_back(current_value);
+        high_nibble = true;
+      }
+    }
+  }
+
+  return data;
+}
+
+
+// Convert base64 data to raw binary.
+static std::vector<uint8_t> decode_base64(const std::string& line)
+{
+  const std::string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+  std::vector<uint8_t> data;
+
+  size_t len = line.size();
+  if (len % 4 != 0) {
+    len = int(len / 4) * 4;
+  }
+
+  for (size_t i = 0; i < len; i += 4) {
+    uint8_t buf[4];
+    for (uint8_t j = 0; j < 4; j++) {
+      size_t k = base64.find(line[i + j]);
+      buf[j] = (uint8_t)(k == std::string::npos ? base64.size() : k);
+    }
+
+    data.push_back((uint8_t)(buf[0] << 2) + (buf[1] >> 4));
+
+    if (line[i + 2] != '=') {
+      data.push_back((uint8_t)((buf[1] & 0x0f) << 4) + (buf[2] >> 2));
+    }
+
+    if (line[i + 3] != '=') {
+      data.push_back((uint8_t)((buf[2] & 0x03) << 6) + buf[3]);
+    }
+  }
+
+  return data;
+}
+
+
+// Parse metadata from WebVMT sync commmands
+static std::vector<uint8_t> parse_vmt_sync_data(const std::string& content)
+{
+  std::vector<uint8_t> data;
+
+  std::regex pattern_sync(R"(\s*\{\s*\"sync\"\s*:\s*\{(.*?)\}\s*\}\s*)");
+  const std::sregex_token_iterator ti_end;
+
+  for (std::sregex_token_iterator ti(content.begin(), content.end(), pattern_sync, 1); ti != ti_end; ++ti)
+  {
+    std::string sync = *ti;
+    std::regex pattern_type(R"(.*\"type\"\s*:\s*\"(.*?)\".*)");
+    std::regex pattern_data(R"(.*\"data\"\s*:\s*\"(.*?)\".*)");
+    std::smatch match;
+
+    if (std::regex_match(sync, match, pattern_type)) {
+      std::string type = match[1];
+
+      std::regex pattern_hex(R"(.*\.hex$)");
+      std::regex pattern_b64(R"(.*\.base64$)");
+
+      std::string textData;
+      if (std::regex_match(sync, match, pattern_data)) {
+        textData = match[1];
+      }
+
+      if (std::regex_match(type, match, pattern_hex)) {
+        std::vector<uint8_t> binaryData = hex_to_binary(textData);
+        data.insert(data.end(), binaryData.begin(), binaryData.end());
+      }
+      else if (std::regex_match(type, match, pattern_b64)) {
+        std::vector<uint8_t> binaryData = decode_base64(textData);
+        data.insert(data.end(), binaryData.begin(), binaryData.end());
+      }
+      else {
+        data.insert(data.end(), textData.data(), textData.data() + textData.length());
+      }
+    }
+  }
+
+  return data;
+}
+
+
+static uint32_t parse_vmt_timestamp(const std::string& vmt_time)
+{
+  std::regex pattern(R"(-?((\d*):)?(\d\d):(\d\d)(\.(\d*))?)");
+  std::smatch match;
+
+  if (!std::regex_match(vmt_time, match, pattern)) {
+    return 0; // no match
+  }
+
+  std::string hh = match[2]; // optional
+  std::string mm = match[3];
+  std::string ss = match[4];
+  std::string fs = match[6]; // optional
+
+  if (vmt_time.find('-') != std::string::npos) {
+    return 0; // negative time not supported
+  }
+
+  uint32_t ms = 0;
+
+  if (fs != "") {
+    if (fs.length() == 3) {
+      ms = std::stoi(fs);
+    }
+    else {
+      return BAD_VMT_TIMESTAMP; // invalid
+    }
+  }
+
+  uint32_t ts = ((hh != "" ? std::stoi(hh) : 0) * 3600 * 1000 +
+                 std::stoi(mm) * 60 * 1000 +
+                 std::stoi(ss) * 1000 +
+                 ms);
+
+  return ts;
+}
+
+int encode_vmt_metadata_track(heif_context* context, heif_track* visual_track,
+                              const std::string& vmt_metadata_file,
+                              const std::string& track_uri, bool binary)
+{
+  // --- add metadata track
+
+  heif_track* track = nullptr;
+
+  heif_track_options* track_options = heif_track_options_alloc();
+  heif_track_options_set_timescale(track_options, 1000);
+
+  heif_context_add_uri_metadata_sequence_track(context, track_uri.c_str(), track_options, &track);
+  heif_raw_sequence_sample* sample = heif_raw_sequence_sample_alloc();
+
+
+  std::ifstream istr(vmt_metadata_file.c_str());
+
+  std::regex pattern_cue(R"(^\s*(-?(\d|:|\.)*)\s*-->\s*(-?(\d|:|\.)*)?.*)");
+  std::regex pattern_note(R"(^\s*(NOTE).*)");
+
+  static std::vector<uint8_t> prev_metadata;
+  static std::optional<uint32_t> prev_ts;
+
+  std::string line;
+  while (std::getline(istr, line))
+  {
+    std::smatch match;
+
+    if (std::regex_match(line, match, pattern_note)) {
+      while (std::getline(istr, line)) {
+        if (line.empty()) {
+          break;
+        }
+      }
+
+      continue;
+    }
+
+    if (!std::regex_match(line, match, pattern_cue)) {
+      continue;
+    }
+
+    std::string cue_start = match[1];
+    std::string cue_end = match[3]; // == "" for unbounded cues
+
+    uint32_t ts = parse_vmt_timestamp(cue_start);
+
+    std::vector<uint8_t> concat;
+
+    if (binary) {
+      while (std::getline(istr, line)) {
+        if (line.empty()) {
+          break;
+        }
+
+        std::vector<uint8_t> binaryData = hex_to_binary(line);
+        concat.insert(concat.end(), binaryData.begin(), binaryData.end());
+      }
+
+    }
+    else {
+      while (std::getline(istr, line)) {
+        if (line.empty()) {
+          break;
+        }
+
+        concat.insert(concat.end(), line.data(), line.data() + line.length());
+        concat.push_back('\n');
+      }
+
+      concat.push_back(0);
+      std::string content(concat.begin(), concat.end());
+      concat = parse_vmt_sync_data(content);
+    }
+
+    if (ts != BAD_VMT_TIMESTAMP) {
+
+      if (ts > *prev_ts) {
+        heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size());
+        heif_raw_sequence_sample_set_duration(sample, ts - *prev_ts);
+        heif_track_add_raw_sequence_sample(track, sample);
+      }
+      else if (ts == *prev_ts) {
+        concat.insert(concat.begin(), prev_metadata.begin(), prev_metadata.end());
+      }
+      else {
+        std::cerr << "Bad WebVMT timestamp order: " << cue_start << "\n";
+      }
+
+      prev_ts = ts;
+      prev_metadata = concat;
+    }
+    else {
+      std::cerr << "Bad WebVMT timestamp: " << cue_start << "\n";
+    }
+  }
+
+  // --- flush last metadata packet
+
+  heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size());
+  heif_raw_sequence_sample_set_duration(sample, 1);
+  heif_track_add_raw_sequence_sample(track, sample);
+
+  // --- add track reference
+
+  heif_track_add_reference_to_track(track, heif_track_reference_type_description, visual_track);
+
+  // --- release all objects
+
+  heif_raw_sequence_sample_release(sample);
+  heif_track_options_release(track_options);
+  heif_track_release(track);
+
+  return 0;
+}
diff --git a/examples/vmt.h b/examples/vmt.h
new file mode 100644
index 00000000..c0b547ba
--- /dev/null
+++ b/examples/vmt.h
@@ -0,0 +1,37 @@
+/*
+  libheif example application "heif-enc" - VMT metadata track support.
+
+  MIT License
+
+  Copyright (c) 2017 Dirk Farin <dirk.farin@gmail.com>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#pragma once
+
+#include <string>
+#include <cstdint>
+
+struct heif_context;
+struct heif_track;
+
+int encode_vmt_metadata_track(heif_context* context, heif_track* visual_track,
+                              const std::string& vmt_metadata_file,
+                              const std::string& track_uri, bool binary);