Add a --jobs flag to avifgainmaputil for multithreading support. (#3115)
Default to using all available cores like avifenc/avifdec.
Also anable automatic tiling (avifenc default, not configurable for now in avifgainmaputil).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55e3bf6..a6894aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,9 +8,15 @@
## [Unreleased]
+### Added since 1.4.1
+
+* Add --jobs flag to avifgainmaputil to use multiple worker threads when
+ reading/writing AVIF files.
+
### Changed since 1.4.1
* Require C11 for compilation. Public headers will remain C99.
+* Add --jobs flag to avifgainmaputil and enable auto tiling.
## [1.4.1] - 2026-03-20
diff --git a/apps/avifenc.c b/apps/avifenc.c
index be8ad96..b81194b 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -1143,7 +1143,7 @@
char manualTilingStr[128];
snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", encoder->tileRowsLog2, encoder->tileColsLog2);
-
+ // Note: this is mirrored in apps/avifgainmaputil/imageio.cc, changes here may be mirrored there if relevant.
printf("Encoding with initial settings: codec '%s' speed [%s], color quality [%d (%s)], alpha quality [%d (%s)]%s, %s, %d worker thread(s), please wait...\n",
codecName ? codecName : "none",
speedStr,
diff --git a/apps/avifgainmaputil/combine_command.cc b/apps/avifgainmaputil/combine_command.cc
index 654316d..ce59cb6 100644
--- a/apps/avifgainmaputil/combine_command.cc
+++ b/apps/avifgainmaputil/combine_command.cc
@@ -76,6 +76,7 @@
"expressed as: MaxCLL,MaxPALL.");
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/true);
arg_image_read_.Init(argparse_);
+ arg_jobs_.Init(argparse_);
}
avifResult CombineCommand::Run() {
@@ -95,9 +96,10 @@
arg_base_cicp_.value().transfer_characteristics;
base_image->matrixCoefficients = arg_base_cicp_.value().matrix_coefficients;
}
- avifResult result = ReadImage(
- base_image.get(), arg_base_filename_, pixel_format, arg_image_read_.depth,
- arg_image_read_.ignore_profile, /*ignore_gain_map=*/true);
+ avifResult result =
+ ReadImage(base_image.get(), arg_base_filename_, pixel_format,
+ arg_image_read_.depth, arg_image_read_.ignore_profile,
+ /*ignore_gain_map=*/true, arg_jobs_.jobs.value());
if (result != AVIF_RESULT_OK) {
std::cout << "Failed to read base image: " << avifResultToString(result)
<< "\n";
@@ -114,7 +116,8 @@
}
result =
ReadImage(alternate_image.get(), arg_alternate_filename_, pixel_format,
- arg_image_read_.depth, arg_image_read_.ignore_profile);
+ arg_image_read_.depth, arg_image_read_.ignore_profile,
+ /*ignore_gain_map=*/true, arg_jobs_.jobs.value());
if (result != AVIF_RESULT_OK) {
std::cout << "Failed to read alternate image: "
<< avifResultToString(result) << "\n";
@@ -189,6 +192,8 @@
encoder->qualityAlpha = arg_image_encode_.quality_alpha;
encoder->qualityGainMap = arg_gain_map_quality_;
encoder->speed = arg_image_encode_.speed;
+ encoder->maxThreads = arg_jobs_.jobs.value();
+ encoder->autoTiling = true; // Match avifenc default.
result =
WriteAvifGrid(base_image.get(), arg_image_encode_.grid.value().grid_cols,
arg_image_encode_.grid.value().grid_rows, encoder.get(),
diff --git a/apps/avifgainmaputil/combine_command.h b/apps/avifgainmaputil/combine_command.h
index 9f1feec..d530522 100644
--- a/apps/avifgainmaputil/combine_command.h
+++ b/apps/avifgainmaputil/combine_command.h
@@ -29,6 +29,7 @@
argparse::ArgValue<float> arg_max_headroom_;
BasicImageEncodeArgs arg_image_encode_;
ImageReadArgs arg_image_read_;
+ JobsArgs arg_jobs_;
};
} // namespace avif
diff --git a/apps/avifgainmaputil/convert_command.cc b/apps/avifgainmaputil/convert_command.cc
index 06bb949..1cbe74e 100644
--- a/apps/avifgainmaputil/convert_command.cc
+++ b/apps/avifgainmaputil/convert_command.cc
@@ -36,6 +36,7 @@
"expressed as: MaxCLL,MaxPALL.");
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/false);
arg_image_read_.Init(argparse_);
+ arg_jobs_.Init(argparse_);
}
avifResult ConvertCommand::Run() {
@@ -61,7 +62,7 @@
avifResult result =
ReadImage(image.get(), arg_input_filename_.value(), pixel_format,
arg_image_read_.depth, arg_image_read_.ignore_profile,
- /*ignore_gain_map*/ false);
+ /*ignore_gain_map=*/false, arg_jobs_.jobs.value());
if (result != AVIF_RESULT_OK) {
std::cout << "Failed to decode image: " << arg_input_filename_;
return result;
@@ -112,6 +113,8 @@
encoder->qualityAlpha = arg_image_encode_.quality_alpha;
encoder->qualityGainMap = arg_gain_map_quality_;
encoder->speed = arg_image_encode_.speed;
+ encoder->maxThreads = arg_jobs_.jobs.value();
+ encoder->autoTiling = true; // Match avifenc default.
result = WriteAvifGrid(image.get(), arg_image_encode_.grid.value().grid_cols,
arg_image_encode_.grid.value().grid_rows,
encoder.get(), arg_output_filename_);
diff --git a/apps/avifgainmaputil/convert_command.h b/apps/avifgainmaputil/convert_command.h
index a8011ba..778b514 100644
--- a/apps/avifgainmaputil/convert_command.h
+++ b/apps/avifgainmaputil/convert_command.h
@@ -26,6 +26,7 @@
argparse::ArgValue<int> arg_gain_map_pixel_format_;
BasicImageEncodeArgs arg_image_encode_;
ImageReadArgs arg_image_read_;
+ JobsArgs arg_jobs_;
};
} // namespace avif
diff --git a/apps/avifgainmaputil/extractgainmap_command.cc b/apps/avifgainmaputil/extractgainmap_command.cc
index 42e3f8b..6ec9e5e 100644
--- a/apps/avifgainmaputil/extractgainmap_command.cc
+++ b/apps/avifgainmaputil/extractgainmap_command.cc
@@ -14,6 +14,7 @@
argparse_.add_argument(arg_input_filename_, "input_filename");
argparse_.add_argument(arg_output_filename_, "output_filename");
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/false);
+ arg_jobs_.Init(argparse_);
}
avifResult ExtractGainMapCommand::Run() {
@@ -21,6 +22,7 @@
if (decoder == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
+ decoder->maxThreads = arg_jobs_.jobs.value();
decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_GAIN_MAP;
avifResult result =
@@ -36,10 +38,11 @@
return AVIF_RESULT_INVALID_ARGUMENT;
}
- return WriteImage(
- decoder->image->gainMap->image, arg_image_encode_.grid.value().grid_cols,
- arg_image_encode_.grid.value().grid_rows, arg_output_filename_,
- arg_image_encode_.quality, arg_image_encode_.speed);
+ return WriteImage(decoder->image->gainMap->image,
+ arg_image_encode_.grid.value().grid_cols,
+ arg_image_encode_.grid.value().grid_rows,
+ arg_output_filename_, arg_image_encode_.quality,
+ arg_image_encode_.speed, arg_jobs_.jobs.value());
}
} // namespace avif
diff --git a/apps/avifgainmaputil/extractgainmap_command.h b/apps/avifgainmaputil/extractgainmap_command.h
index 9872a18..959e503 100644
--- a/apps/avifgainmaputil/extractgainmap_command.h
+++ b/apps/avifgainmaputil/extractgainmap_command.h
@@ -18,6 +18,7 @@
argparse::ArgValue<std::string> arg_input_filename_;
argparse::ArgValue<std::string> arg_output_filename_;
BasicImageEncodeArgs arg_image_encode_;
+ JobsArgs arg_jobs_;
};
} // namespace avif
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 1686f55..d50a862 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -24,7 +24,7 @@
avifResult WriteImage(const avifImage* image, int grid_cols, int grid_rows,
const std::string& output_filename, int quality,
- int speed) {
+ int speed, int jobs) {
quality = Clamp(quality, 0, 100);
speed = Clamp(speed, 0, 10);
const avifAppFileFormat output_format =
@@ -55,6 +55,7 @@
}
encoder->quality = quality;
encoder->speed = speed;
+ encoder->maxThreads = jobs;
return WriteAvifGrid(image, grid_cols, grid_rows, encoder.get(),
output_filename);
} else {
@@ -65,6 +66,47 @@
return AVIF_RESULT_OK;
}
+namespace {
+std::string QualityLevelString(int quality) {
+ if (quality == AVIF_QUALITY_LOSSLESS) {
+ return "Lossless";
+ }
+ if (quality >= 80) {
+ return "High";
+ }
+ if (quality >= 50) {
+ return "Medium";
+ }
+ if (quality == AVIF_QUALITY_WORST) {
+ return "Worst";
+ }
+ return "Low";
+}
+
+// Based on avifenc.c, changes here may be mirrored there if relevant.
+void PrintEncodingSettings(const avifEncoder* encoder, bool has_gain_map) {
+ std::string gain_map_str;
+ if (has_gain_map) {
+ gain_map_str = ", gain map quality [" +
+ std::to_string(encoder->qualityGainMap) + " (" +
+ QualityLevelString(encoder->qualityGainMap) + ")]";
+ }
+ std::string manual_tiling_str =
+ "tileRowsLog2 [" + std::to_string(encoder->tileRowsLog2) +
+ "], tileColsLog2 [" + std::to_string(encoder->tileColsLog2) + "]";
+ std::cout << "Encoding AVIF with settings: codec '"
+ << avifCodecName(encoder->codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE)
+ << "' speed ['" << encoder->speed << "'], color quality ['"
+ << encoder->quality << "' (" << QualityLevelString(encoder->quality)
+ << ")], alpha quality ['" << encoder->qualityAlpha << "' ("
+ << QualityLevelString(encoder->qualityAlpha) << ")]" << gain_map_str
+ << ", "
+ << (encoder->autoTiling ? "automatic tiling" : manual_tiling_str)
+ << ", " << encoder->maxThreads
+ << " worker thread(s), please wait...\n";
+}
+} // namespace
+
avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
const std::string& output_filename) {
avifRWData encoded = AVIF_DATA_EMPTY;
@@ -72,8 +114,7 @@
avifImageDump(image,
/*gridCols=*/1,
/*gridRows=*/1, AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
- std::cout << "Encoding AVIF at quality " << encoder->quality << " speed "
- << encoder->speed << ", please wait...\n";
+ PrintEncodingSettings(encoder, image->gainMap != nullptr);
avifResult result = avifEncoderWrite(encoder, image, &encoded);
if (result != AVIF_RESULT_OK) {
std::cerr << "Failed to encode image: " << avifResultToString(result)
@@ -122,8 +163,7 @@
std::cout << "AVIF to be written:\n";
avifImageDump(image, grid_cols, grid_rows,
AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
- std::cout << "Encoding AVIF at quality " << encoder->quality << " speed "
- << encoder->speed << ", please wait...\n";
+ PrintEncodingSettings(encoder, image->gainMap != nullptr);
avifResult result = avifEncoderAddImageGrid(
encoder, grid_cols, grid_rows,
const_cast<const avifImage* const*>(grid_cells_ptrs.data()),
@@ -155,7 +195,7 @@
avifResult ReadImage(avifImage* image, const std::string& input_filename,
avifPixelFormat requested_format, uint32_t requested_depth,
- bool ignore_profile, bool ignore_gain_map) {
+ bool ignore_profile, bool ignore_gain_map, int jobs) {
avifAppFileFormat input_format = avifGuessFileFormat(input_filename.c_str());
if (input_format == AVIF_APP_FILE_FORMAT_UNKNOWN) {
std::cerr << "Cannot determine input format: " << input_filename;
@@ -168,6 +208,7 @@
if (!ignore_gain_map) {
decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
}
+ decoder->maxThreads = jobs;
avifResult result = ReadAvif(decoder.get(), input_filename, ignore_profile);
if (result != AVIF_RESULT_OK) {
return result;
diff --git a/apps/avifgainmaputil/imageio.h b/apps/avifgainmaputil/imageio.h
index c3d494b..bc69c63 100644
--- a/apps/avifgainmaputil/imageio.h
+++ b/apps/avifgainmaputil/imageio.h
@@ -15,11 +15,11 @@
// other formats.
avifResult WriteImage(const avifImage* image, int grid_cols, int grid_rows,
const std::string& output_filename, int quality,
- int speed);
+ int speed, int jobs);
// Reads an image in any of the supported formats. Ignores any gain map.
avifResult ReadImage(avifImage* image, const std::string& input_filename,
avifPixelFormat requested_format, uint32_t requested_depth,
- bool ignore_profile, bool ignore_gain_map = true);
+ bool ignore_profile, bool ignore_gain_map, int jobs);
// Reads an image in avif format given a pre-configured encoder.
avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
diff --git a/apps/avifgainmaputil/printmetadata_command.cc b/apps/avifgainmaputil/printmetadata_command.cc
index 6f98aa8..93350e9 100644
--- a/apps/avifgainmaputil/printmetadata_command.cc
+++ b/apps/avifgainmaputil/printmetadata_command.cc
@@ -34,6 +34,7 @@
: ProgramCommand("printmetadata",
"Print the metadata of the gain map of an AVIF file") {
argparse_.add_argument(arg_input_filename_, "input_filename");
+ arg_jobs_.Init(argparse_);
}
avifResult PrintMetadataCommand::Run() {
@@ -41,6 +42,7 @@
if (decoder == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
+ decoder->maxThreads = arg_jobs_.jobs.value();
avifResult result =
avifDecoderSetIOFile(decoder.get(), arg_input_filename_.value().c_str());
diff --git a/apps/avifgainmaputil/printmetadata_command.h b/apps/avifgainmaputil/printmetadata_command.h
index 41d3e8e..ac968fb 100644
--- a/apps/avifgainmaputil/printmetadata_command.h
+++ b/apps/avifgainmaputil/printmetadata_command.h
@@ -16,6 +16,7 @@
private:
argparse::ArgValue<std::string> arg_input_filename_;
+ JobsArgs arg_jobs_;
};
} // namespace avif
diff --git a/apps/avifgainmaputil/program_command.cc b/apps/avifgainmaputil/program_command.cc
index 5da3040..ab1c714 100644
--- a/apps/avifgainmaputil/program_command.cc
+++ b/apps/avifgainmaputil/program_command.cc
@@ -2,6 +2,8 @@
#include <vector>
+#include "avifutil.h"
+
namespace avif {
ProgramCommand::ProgramCommand(const std::string& name,
@@ -109,4 +111,28 @@
std::vector<std::string> GridOptionsConverter::default_choices() { return {}; }
+argparse::ConvertedValue<int> JobsConverter::from_str(const std::string& str) {
+ argparse::ConvertedValue<int> converted_value;
+
+ if (str == "all") {
+ converted_value.set_value(avifQueryCPUCount());
+ } else {
+ int jobs;
+ try {
+ jobs = std::stoi(str);
+ } catch (...) {
+ converted_value.set_error("Invalid jobs value: " + str);
+ return converted_value;
+ }
+ if (jobs < 1) {
+ jobs = 1;
+ }
+ converted_value.set_value(jobs);
+ }
+
+ return converted_value;
+}
+
+std::vector<std::string> JobsConverter::default_choices() { return {}; }
+
} // namespace avif
diff --git a/apps/avifgainmaputil/program_command.h b/apps/avifgainmaputil/program_command.h
index 785479a..b579253 100644
--- a/apps/avifgainmaputil/program_command.h
+++ b/apps/avifgainmaputil/program_command.h
@@ -92,6 +92,13 @@
std::vector<std::string> default_choices();
};
+// Jobs converter for use with argparse. Parses an integer or "all".
+struct JobsConverter {
+ // Methods expected by argparse.
+ argparse::ConvertedValue<int> from_str(const std::string& str);
+ std::vector<std::string> default_choices();
+};
+
// Basic flags for image writing.
struct BasicImageEncodeArgs {
argparse::ArgValue<int> speed;
@@ -143,6 +150,19 @@
}
};
+// Flags for setting the number of worker threads.
+struct JobsArgs {
+ argparse::ArgValue<int> jobs;
+
+ void Init(argparse::ArgumentParser& argparse) {
+ argparse.add_argument<int, JobsConverter>(jobs, "--jobs", "-j")
+ .help(
+ "Number of jobs (worker threads), or 'all' to potentially use as "
+ "many cores as possible.")
+ .default_value("all");
+ }
+};
+
// Helper to parse flags that contain several delimited values.
template <typename T>
bool ParseList(std::string to_parse, char delim, int expected_num,
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index e6e44ec..cff3f77 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -151,6 +151,7 @@
.help(
"Override content light level information for the alternate image "
"in the input image, expressed as: MaxCLL,MaxPALL.");
+ arg_jobs_.Init(argparse_);
}
avifResult SwapBaseCommand::Run() {
@@ -158,6 +159,7 @@
if (decoder == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
+ decoder->maxThreads = arg_jobs_.jobs.value();
decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
arg_image_read_.ignore_profile);
@@ -226,6 +228,8 @@
encoder->qualityAlpha = arg_image_encode_.quality_alpha;
encoder->qualityGainMap = arg_gain_map_quality_;
encoder->speed = arg_image_encode_.speed;
+ encoder->maxThreads = arg_jobs_.jobs.value();
+ encoder->autoTiling = true; // Match avifenc default.
result =
WriteAvifGrid(new_base.get(), arg_image_encode_.grid.value().grid_cols,
arg_image_encode_.grid.value().grid_rows, encoder.get(),
diff --git a/apps/avifgainmaputil/swapbase_command.h b/apps/avifgainmaputil/swapbase_command.h
index bffbbdb..73cf5aa 100644
--- a/apps/avifgainmaputil/swapbase_command.h
+++ b/apps/avifgainmaputil/swapbase_command.h
@@ -29,6 +29,7 @@
ImageReadArgs arg_image_read_;
BasicImageEncodeArgs arg_image_encode_;
argparse::ArgValue<int> arg_gain_map_quality_;
+ JobsArgs arg_jobs_;
};
} // namespace avif
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index 1f2da42..c3057ea 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -50,6 +50,7 @@
"MaxCLL,MaxPALL. Only relevant when saving to AVIF.");
arg_image_read_.Init(argparse_);
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/true);
+ arg_jobs_.Init(argparse_);
}
avifResult TonemapCommand::Run() {
@@ -63,6 +64,7 @@
if (decoder == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
+ decoder->maxThreads = arg_jobs_.jobs.value();
decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
arg_image_read_.ignore_profile);
@@ -215,7 +217,7 @@
return WriteImage(tone_mapped.get(), arg_image_encode_.grid.value().grid_cols,
arg_image_encode_.grid.value().grid_rows,
arg_output_filename_, arg_image_encode_.quality,
- arg_image_encode_.speed);
+ arg_image_encode_.speed, arg_jobs_.jobs.value());
}
} // namespace avif
diff --git a/apps/avifgainmaputil/tonemap_command.h b/apps/avifgainmaputil/tonemap_command.h
index 475b0d4..719b134 100644
--- a/apps/avifgainmaputil/tonemap_command.h
+++ b/apps/avifgainmaputil/tonemap_command.h
@@ -23,6 +23,7 @@
argparse::ArgValue<CicpValues> arg_output_cicp_;
ImageReadArgs arg_image_read_;
BasicImageEncodeArgs arg_image_encode_;
+ JobsArgs arg_jobs_;
};
} // namespace avif