Merge "Handle 64b box sizes" into main
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ce831e6..e44cba0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,10 +13,12 @@
option(AVIFINFO_BUILD_TESTS
"Build and enable tests (GoogleTest must be installed)" OFF)
+option(AVIFINFO_BUILD_TOOLS "Build tools" OFF)
# C library
add_library(avifinfo avifinfo.c)
+target_include_directories(avifinfo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# C++ tests
@@ -26,11 +28,42 @@
enable_testing()
add_executable(avifinfo_test tests/avifinfo_test.cc)
- target_include_directories(avifinfo_test PRIVATE ${GTEST_INCLUDE_DIRS}
- ${CMAKE_CURRENT_SOURCE_DIR})
+ target_include_directories(avifinfo_test PRIVATE ${GTEST_INCLUDE_DIRS})
target_link_libraries(avifinfo_test PRIVATE ${GTEST_BOTH_LIBRARIES} avifinfo)
add_test(
NAME avifinfo_test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/avifinfo_test
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests)
endif()
+
+# C++ tools
+
+if(AVIFINFO_BUILD_TOOLS)
+ find_package(Threads REQUIRED) # For aom
+
+ # Clone and build libavif and its dependency aom.
+ include(ExternalProject)
+ set(EXTERNAL_INSTALL_LOCATION ${CMAKE_BINARY_DIR}/external)
+ ExternalProject_Add(
+ libavif
+ GIT_REPOSITORY https://github.com/AOMediaCodec/libavif
+ PATCH_COMMAND cd ${CMAKE_BINARY_DIR}/libavif-prefix/src/libavif/ext &&
+ ./aom.cmd
+ UPDATE_DISCONNECTED 1 # Avoid building aom everytime.
+ # aom.cmd builds aom as static so libavif should be built as static too.
+ CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_LOCATION}
+ -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON -DBUILD_SHARED_LIBS=OFF)
+
+ add_executable(avifinfo_tool tools/avifinfo_tool.cc tests/avifinfo_fuzz.cc)
+ set_property(TARGET avifinfo_tool PROPERTY CXX_STANDARD 17) # for filesystem
+ target_include_directories(avifinfo_tool SYSTEM
+ PRIVATE ${EXTERNAL_INSTALL_LOCATION}/include)
+ # libavif's CMakeLists.txt uses a PRIVATE target_link_libraries() so
+ # avifinfo_tool must be linked to aom too (and thus also to pthread).
+ target_link_directories(
+ avifinfo_tool PRIVATE ${EXTERNAL_INSTALL_LOCATION}/lib
+ ${CMAKE_BINARY_DIR}/libavif-prefix/src/libavif/ext/aom/build.libavif)
+ target_link_libraries(avifinfo_tool PRIVATE avifinfo avif aom
+ Threads::Threads)
+ add_dependencies(avifinfo_tool libavif)
+endif()
diff --git a/tools/avifinfo_tool.cc b/tools/avifinfo_tool.cc
new file mode 100644
index 0000000..b16281c
--- /dev/null
+++ b/tools/avifinfo_tool.cc
@@ -0,0 +1,271 @@
+// Copyright (c) 2021, Alliance for Open Media. All rights reserved
+//
+// This source code is subject to the terms of the BSD 2 Clause License and
+// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
+// was not distributed with this source code in the LICENSE file, you can
+// obtain it at www.aomedia.org/license/software. If the Alliance for Open
+// Media Patent License 1.0 was not distributed with this source code in the
+// PATENTS file, you can obtain it at www.aomedia.org/license/patent.
+
+#include <algorithm>
+#include <cstring>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "avifinfo.h"
+#include "avif/avif.h"
+
+namespace {
+
+//------------------------------------------------------------------------------
+
+std::string GetHelpStr() {
+ std::string str;
+ str += "Command line tool to compare libavif and libavifinfo results.\n";
+ str += "Usage: avifparse [options] <directory>\n";
+ str += "Options:\n";
+ str += " -h, --help ...... Print this help\n";
+ str += " --fast .......... Skip libavif decoding, only use libavifinfo\n";
+ str += " --min-size ...... Find minimum size to extract features per file\n";
+ str += " --validate ...... Check libavifinfo consistency on each file\n";
+ return str;
+}
+
+//------------------------------------------------------------------------------
+
+// Decoding result.
+struct Result {
+ bool success; // True if the 'features' were correctly decoded.
+ AvifInfoFeatures features;
+};
+
+// Decodes the AVIF at 'data' of 'data_size' bytes using libavif.
+Result DecodeAvif(const uint8_t data[], size_t data_size) {
+ Result result;
+ avifImage* const image = avifImageCreateEmpty();
+ avifDecoder* const decoder = avifDecoderCreate();
+ decoder->strictFlags = AVIF_STRICT_DISABLED;
+ const avifResult status =
+ avifDecoderReadMemory(decoder, image, data, data_size);
+ avifDecoderDestroy(decoder);
+ if (status == AVIF_RESULT_OK) {
+ const uint32_t num_channels =
+ ((image->yuvFormat == AVIF_PIXEL_FORMAT_NONE) ? 0
+ : (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1
+ : 3) +
+ ((image->alphaPlane != nullptr) ? 1 : 0);
+ result = {true, {image->width, image->height, image->depth, num_channels}};
+ } else {
+ result = {false};
+ }
+ avifImageDestroy(image);
+ return result;
+}
+
+// Parses the AVIF at 'data' of 'data_size' bytes using libavifinfo.
+Result ParseAvif(const uint8_t data[], size_t data_size) {
+ Result result;
+ const AvifInfoStatus status = AvifInfoGetWithSize(
+ data, data_size, &result.features, /*file_size=*/data_size);
+ result.success = (status == kAvifInfoOk);
+ return result;
+}
+
+// Same as above but also returns the 'min_data_size' for which 'data' can be
+// successfully parsed.
+Result ParseAvifForSize(const uint8_t data[], size_t data_size,
+ size_t& min_data_size) {
+ const Result result = ParseAvif(data, data_size);
+ if (!result.success) {
+ min_data_size = data_size;
+ return result;
+ }
+ min_data_size = 1;
+ size_t max_data_size = data_size;
+ while (min_data_size < max_data_size) {
+ const size_t middle = (min_data_size + max_data_size) / 2;
+ if (AvifInfoGetWithSize(data, middle, nullptr, /*file_size=*/data_size) ==
+ kAvifInfoOk) {
+ max_data_size = middle;
+ } else {
+ min_data_size = middle + 1;
+ }
+ }
+ return result;
+}
+
+// Reuses the fuzz target for easy library validation.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t data_size);
+
+// Aggregated stats about the decoded/parsed AVIF files.
+struct Stats {
+ uint32_t num_files_invalid_at_decode = 0;
+ uint32_t num_files_invalid_at_parse = 0;
+ uint32_t num_files_invalid_at_both = 0;
+ std::unordered_map<size_t, uint32_t> min_size_to_count;
+};
+
+//------------------------------------------------------------------------------
+
+// Recursively adds all files at 'path' to 'file_paths'.
+void FindFiles(const std::string& path, std::vector<std::string>& file_paths) {
+ if (std::filesystem::is_directory(path)) {
+ for (const std::filesystem::directory_entry& entry :
+ std::filesystem::directory_iterator(path)) {
+ FindFiles(entry.path(), file_paths);
+ }
+ } else {
+ file_paths.emplace_back(path);
+ }
+}
+
+// Find the longest common prefix of all input 'paths'.
+std::string FindCommonLongestPrefix(const std::vector<std::string>& paths) {
+ std::string prefix = paths.empty() ? "" : paths.front();
+ for (const std::string& path : paths) {
+ const auto mismatch =
+ std::mismatch(prefix.begin(), prefix.end(), path.begin(), path.end());
+ prefix = prefix.substr(0, std::distance(prefix.begin(), mismatch.first));
+ }
+ return std::filesystem::path(prefix).remove_filename().string();
+}
+
+//------------------------------------------------------------------------------
+
+// Uses libavifinfo to extract the features of an AVIF file stored in 'data' at
+// 'path'. The AVIF file is 'data_size'-byte long.
+void ParseFile(const std::string& path, const uint8_t* data, size_t data_size,
+ Stats& stats) {
+ const Result parse = ParseAvif(data, data_size);
+ if (!parse.success) {
+ ++stats.num_files_invalid_at_parse;
+ std::cout << "parsing failure for " << path << std::endl;
+ }
+}
+
+// Uses libavif then libavifinfo to extract the features of an AVIF file.
+void DecodeAndParseFile(const std::string& path, const uint8_t* data,
+ size_t data_size, Stats& stats) {
+ const Result decode = DecodeAvif(data, data_size);
+ const Result parse = ParseAvif(data, data_size);
+ if (!decode.success) ++stats.num_files_invalid_at_decode;
+ if (!parse.success) ++stats.num_files_invalid_at_parse;
+ if (!decode.success && !parse.success) ++stats.num_files_invalid_at_both;
+
+ if (!parse.success ||
+ (decode.success &&
+ (decode.features.width != parse.features.width ||
+ decode.features.height != parse.features.height ||
+ decode.features.bit_depth != parse.features.bit_depth ||
+ decode.features.num_channels != parse.features.num_channels))) {
+ if (decode.success && parse.success) {
+ std::cout << "decoded " << decode.features.width << "x"
+ << decode.features.height << "," << decode.features.bit_depth
+ << "b*" << decode.features.num_channels << " / "
+ << "parsed " << parse.features.width << "x"
+ << parse.features.height << "," << parse.features.bit_depth
+ << "b*" << parse.features.num_channels;
+ } else {
+ std::cout << "decoding " << (decode.success ? "success" : "failure")
+ << " / parsing " << (parse.success ? "success" : "failure");
+ }
+ std::cout << " for " << path << std::endl;
+ }
+}
+
+// Returns the minimum number of bytes of AVIF 'data' for features to be
+// extracted.
+void FindMinSizeOfFile(const std::string& path, const uint8_t* data,
+ size_t data_size, Stats& stats) {
+ size_t min_size;
+ const Result parse = ParseAvifForSize(data, data_size, min_size);
+ if (parse.success) {
+ ++stats.min_size_to_count[min_size];
+ } else {
+ ++stats.num_files_invalid_at_parse;
+ }
+}
+
+// Checks the consistency of libavifinfo over an AVIF file.
+void ValidateFile(const std::string& path, const uint8_t* data,
+ size_t data_size) {
+ if (LLVMFuzzerTestOneInput(data, data_size) != 0) {
+ std::cout << "validation failed for " << path << std::endl;
+ }
+}
+
+} // namespace
+
+//------------------------------------------------------------------------------
+
+int main(int argc, char** argv) {
+ std::vector<std::string> file_paths;
+ bool only_parse = false;
+ bool find_min_size = false;
+ bool validate = false;
+
+ for (int arg = 1; arg < argc; ++arg) {
+ if (!std::strcmp(argv[arg], "-h")) {
+ std::cout << GetHelpStr();
+ return 0;
+ } else if (!std::strcmp(argv[arg], "--fast")) {
+ only_parse = true;
+ } else if (!std::strcmp(argv[arg], "--min-size")) {
+ find_min_size = true;
+ only_parse = true;
+ } else if (!std::strcmp(argv[arg], "--validate")) {
+ validate = true;
+ } else {
+ FindFiles(argv[arg], file_paths);
+ }
+ }
+ if (file_paths.empty()) {
+ std::cerr << "No input specified" << std::endl;
+ return 1;
+ }
+ std::cout << "Found " << file_paths.size() << " files" << std::endl;
+ const std::string prefix = FindCommonLongestPrefix(file_paths);
+ for (std::string& file_path : file_paths) {
+ file_path = file_path.substr(prefix.size());
+ }
+
+ Stats stats;
+ for (const std::string& file_path : file_paths) {
+ std::vector<uint8_t> bytes(std::filesystem::file_size(prefix + file_path));
+ std::ifstream file(prefix + file_path, std::ios::binary);
+ file.read(reinterpret_cast<char*>(bytes.data()), bytes.size());
+ if (find_min_size) {
+ FindMinSizeOfFile(file_path, bytes.data(), bytes.size(), stats);
+ } else if (only_parse) {
+ ParseFile(file_path, bytes.data(), bytes.size(), stats);
+ } else {
+ DecodeAndParseFile(file_path, bytes.data(), bytes.size(), stats);
+ }
+ if (validate) {
+ ValidateFile(file_path, bytes.data(), bytes.size());
+ }
+ }
+
+ std::cout << stats.num_files_invalid_at_parse << " files failed to parse"
+ << std::endl;
+ if (!only_parse) {
+ std::cout << stats.num_files_invalid_at_decode << " files failed to decode"
+ << std::endl;
+ std::cout << stats.num_files_invalid_at_both
+ << " files failed to parse and decode" << std::endl;
+ }
+
+ if (find_min_size) {
+ std::cout << std::endl;
+ for (const auto& it : stats.min_size_to_count) {
+ std::cout << it.second << " files need " << it.first
+ << " bytes to extract features" << std::endl;
+ }
+ }
+ return 0;
+}