blob: ddece7ad6a2213f8500529fa6d4ac984bf331933 [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// -----------------------------------------------------------------------------
//
// Helper functions used by examples.
#include "./example_utils.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <dirent.h>
#endif // _WIN32
#include <cstdio>
#include <string>
#include <vector>
#include "examples/stopwatch.h"
#include "src/common/color_precision.h"
#include "src/wp2/base.h"
#include "src/wp2/decode.h"
namespace WP2 {
#if defined(WP2_GIT_VERSION)
const char* const GitVersion = WP2_GIT_VERSION;
#else
const char* const GitVersion = "0";
#endif
//------------------------------------------------------------------------------
// Prints the distortion stored in 'values'.
static void PrintDistortion(MetricType metric, float values[5],
size_t data_size, bool short_output) {
if (!short_output) {
fprintf(stderr, "%s: ", kMetricNames[metric]);
if (metric == PSNR_YUV || metric == SSIM_YUV || metric == PSNRHVS) {
fprintf(stderr, "A: %2.2f Y: %2.2f U: %2.2f V: %2.2f ", values[0],
values[1], values[2], values[3]);
} else {
fprintf(stderr, "A: %2.2f R: %2.2f G: %2.2f B: %2.2f ", values[0],
values[1], values[2], values[3]);
}
fprintf(stderr, "Total size: %7d %s: %2.2f\n", (int)data_size,
kMetricNames[metric], values[4]);
} else {
fprintf(stderr, "%7d %.4f\n", (int)data_size, values[4]);
}
}
WP2Status PrintDistortion(const std::vector<ArgbBuffer>& refs,
const uint8_t* const data, size_t data_size,
MetricType metric, bool short_output) {
ArrayDecoder decoder(data, data_size);
uint32_t num_frames = 0;
float avg_values[5] = {0};
while (decoder.ReadFrame()) {
CHECK_TRUE(refs.size() > num_frames,
"Round trip error!! This shouldn't happen.");
float values[5];
CHECK_STATUS(decoder.GetPixels().GetDistortionBlackOrWhiteBackground(
refs[num_frames], metric, values),
"Error while computing distortion!");
if (!short_output) {
fprintf(stderr, "Frame %3u distortion ", num_frames);
PrintDistortion(metric, values, data_size, short_output);
}
for (int i = 0; i < 5; ++i) avg_values[i] += values[i];
++num_frames;
}
CHECK_STATUS(decoder.GetStatus(),
"Round trip error!! This shouldn't happen.");
if (num_frames > 0) {
// we only want to print the average stats if there's more than 1
// frame (since the stats for frame #0 are already printed above).
if (num_frames > 1) {
for (int i = 0; i < 5; ++i) avg_values[i] /= num_frames;
if (!short_output) {
fprintf(stderr, "Total avg distortion over %u frames ", num_frames);
}
PrintDistortion(metric, avg_values, data_size, short_output);
}
} else {
float values[5];
CHECK_TRUE(refs.size() == 1, "Round trip error!! This shouldn't happen.");
CHECK_STATUS(decoder.GetPixels().GetDistortionBlackOrWhiteBackground(
refs.front(), metric, values),
"Error while computing distortion!");
PrintDistortion(metric, values, data_size, short_output);
}
return WP2_STATUS_OK;
}
WP2Status PrintDistortion(const ArgbBuffer& ref, const uint8_t* const data,
size_t data_size, MetricType metric,
bool short_output) {
std::vector<ArgbBuffer> refs(1);
CHECK_STATUS(refs.front().SetView(ref), "SetView failed");
return PrintDistortion(refs, data, data_size, metric, short_output);
}
//------------------------------------------------------------------------------
void ConvertToGray(ArgbBuffer* const pic) {
assert(pic != nullptr);
for (uint32_t y = 0; y < pic->height(); ++y) {
uint8_t* const row = pic->GetRow8(y);
for (uint32_t x = 0; x < pic->width(); ++x) {
const uint32_t r = row[4 * x + 1];
const uint32_t g = row[4 * x + 2];
const uint32_t b = row[4 * x + 3];
// We use BT.709 for converting to luminance.
const uint8_t Y = std::lroundf(0.2126f * r + 0.7152f * g + 0.0722f * b);
row[4 * x + 1] = row[4 * x + 2] = row[4 * x + 3] = Y;
}
}
}
//------------------------------------------------------------------------------
WP2Status CollectBitTraces(const uint8_t* const data, size_t data_size,
int bit_trace, uint32_t bit_trace_level,
bool short_output, bool show_histograms) {
DecoderConfig config;
DecoderInfo info;
config.info = &info;
ArrayDecoder decoder(data, data_size, config);
while (decoder.ReadFrame()) continue; // Decode everything.
CHECK_STATUS(decoder.GetStatus(),
"Round trip error!! This shouldn't happen.");
#if defined(WP2_BITTRACE)
PrintBitTraces(info, data_size, /*sort_value=*/true,
/*use_bytes=*/bit_trace == 2, show_histograms,
/*short_version=*/short_output, bit_trace_level);
#else
(void)bit_trace;
(void)bit_trace_level;
(void)short_output;
fprintf(stderr,
"Bit traces are not available without "
"WP2_BITTRACE compile flag.\n");
#endif // WP2_BITTRACE
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
namespace {
// Remembers the location of a chunk in the bitstream or in a hard-coded array.
class ChunkWatcher : public Writer {
public:
bool Reserve(size_t) override { return true; }
bool Append(const void* data, size_t data_size) override {
if (ptr_ == nullptr) ptr_ = (const uint8_t*)data;
size_ += data_size;
++num_append_calls_;
return true;
}
const uint8_t* ptr_ = nullptr;
size_t size_ = 0;
uint32_t num_append_calls_ = 0;
};
// Returns a string containing info about a 'chunk_type' in 'input_data_bytes'.
std::string PrintChunk(const uint8_t input_data_bytes[],
size_t input_data_size, ChunkType chunk_type,
uint32_t frame_index = 0) {
ChunkWatcher chunk;
const WP2Status status = GetChunk(input_data_bytes, input_data_size,
chunk_type, &chunk, frame_index);
if (status != WP2_STATUS_OK) {
return SPrintf("ERROR (%s)\n", WP2GetStatusMessage(status));
} else if (chunk.ptr_ == nullptr) {
return "none\n";
} else if (chunk.ptr_ >= input_data_bytes &&
chunk.ptr_ < input_data_bytes + input_data_size &&
chunk.num_append_calls_ == 1) {
return SPrintf("%7u bytes at offset %7u\n", chunk.size_,
chunk.ptr_ - input_data_bytes);
} else {
// Special hard-coded ICC case.
return SPrintf("1 byte expanded into %7u bytes\n", chunk.size_);
}
}
} // namespace
std::string PrintSummary(const uint8_t input_data_bytes[],
size_t input_data_size, bool short_output) {
BitstreamFeatures features;
WP2Status status = features.Read(input_data_bytes, input_data_size);
if (status != WP2_STATUS_OK) {
return SPrintf("ERROR: %s (in header)\n", WP2GetStatusMessage(status));
}
const Argb32b prev_col = ToArgb32b(features.preview_color);
const uint32_t degrees = (uint32_t)features.orientation * 90;
std::string str;
str += SPrintf("Dimension: %u x %u\n", features.width, features.height);
if (!short_output) {
str += SPrintf(
"animation: %s\n",
features.is_animation
? (features.loop_forever ? "yes (loop forever)" : "yes (play once)")
: "no");
str += SPrintf("preview color: %u, %u, %u\n", prev_col.r, prev_col.g,
prev_col.b);
str += SPrintf("rotated by: %u degrees clockwise\n", degrees);
str += SPrintf("transfer: %d\n", (int)features.transfer_function);
str += SPrintf("file size: %u\n", (uint32_t)input_data_size);
str += "\nChunks:\n";
str += " header: " +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kHeader);
str += " preview: " +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kPreview);
str += " ICC: " +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kIcc);
size_t num_frames;
status = GetNumFrames(input_data_bytes, input_data_size, &num_frames);
if (status == WP2_STATUS_OK) {
for (uint32_t i = 0; i < (uint32_t)num_frames; ++i) {
str +=
(features.is_animation ? SPrintf(" frame %5u: ", i + 1)
: " pixels: ") +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kFrame, i);
}
} else {
str +=
SPrintf(" pixels: ERROR (%s)\n", WP2GetStatusMessage(status));
}
str += " XMP: " +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kXmp);
str += " EXIF: " +
PrintChunk(input_data_bytes, input_data_size, ChunkType::kExif);
}
return str;
}
//------------------------------------------------------------------------------
#ifdef _WIN32
static constexpr char kPathSeparator = (char)'\\';
#else
static constexpr char kPathSeparator = (char)'/';
#endif // _WIN32
std::string JoinPath(const std::string& prefix, const std::string& suffix) {
std::string joined_path = prefix;
if (!joined_path.empty() && joined_path.back() != kPathSeparator) {
joined_path += kPathSeparator;
}
joined_path += suffix;
return joined_path;
}
bool IsDirectory(const std::string& path) {
if (path.empty()) return false;
#ifdef _WIN32
auto attributes = GetFileAttributes(path.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES) return false;
return (attributes == FILE_ATTRIBUTE_DIRECTORY);
#else
DIR* dir = opendir(path.c_str());
if (dir == nullptr) return false;
(void)closedir(dir);
return true;
#endif // _WIN32
}
bool GetFilesIn(const std::string& dir_path, FileList* const files,
bool recursive) {
#ifdef _WIN32
WIN32_FIND_DATA ffd;
HANDLE handle = FindFirstFile(JoinPath(dir_path, "*").c_str(), &ffd);
if (handle == INVALID_HANDLE_VALUE) return false;
do {
const std::string file_path = JoinPath(dir_path, ffd.cFileName);
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (recursive) GetFilesIn(file_path, files, recursive);
} else {
files->push_back(file_path);
}
} while (FindNextFile(handle, &ffd) != 0);
if (GetLastError() != ERROR_NO_MORE_FILES) {
FindClose(handle);
return false;
}
FindClose(handle);
return true;
#else
DIR* dir = opendir(dir_path.c_str());
if (dir == nullptr) return false;
struct dirent* ent;
while ((ent = readdir(dir)) != nullptr) {
if ((strcmp(ent->d_name, ".") == 0) || (strcmp(ent->d_name, "..") == 0)) {
continue;
}
const std::string ent_path = JoinPath(dir_path, ent->d_name);
if (recursive) {
if (!GetFilesIn(ent_path, files, recursive)) {
files->push_back(ent_path);
}
} else if (!IsDirectory(ent_path)) {
files->push_back(ent_path);
}
}
closedir(dir);
return true;
#endif // _WIN32
}
std::string GetFileName(const std::string& path) {
const size_t last_separator_pos = path.find_last_of(kPathSeparator);
if (last_separator_pos == std::string::npos) return path;
return path.substr(last_separator_pos + 1);
}
std::string RemoveFileExtension(const std::string& path) {
const size_t last_separator_pos = path.find_last_of(kPathSeparator);
const size_t last_dot_pos = path.find_last_of('.');
if ((last_dot_pos == std::string::npos) ||
((last_separator_pos != std::string::npos) &&
(last_separator_pos > last_dot_pos))) {
return path;
}
return path.substr(0, last_dot_pos);
}
std::string GetFileExtension(const std::string& path) {
const size_t last_separator_pos = path.find_last_of(kPathSeparator);
const size_t last_dot_pos = path.find_last_of('.');
if ((last_dot_pos == std::string::npos) ||
(last_dot_pos + 1 == path.length()) ||
((last_separator_pos != std::string::npos) &&
(last_separator_pos > last_dot_pos))) {
return "";
}
return path.substr(last_dot_pos + 1);
}
std::string InputFileToOutputDir(const std::string& input_file_path,
const std::string& output_dir_path,
const std::string& extension) {
assert(!extension.empty());
return JoinPath(
output_dir_path,
RemoveFileExtension(GetFileName(input_file_path)) + '.' + extension);
}
//------------------------------------------------------------------------------
// Handy class just for collecting all params / work variable in a single place.
struct PartitionParams {
public:
const BlockSize* valid_blocks;
uint32_t max_width, max_height;
bool skip_invalid;
uint32_t split_count;
std::vector<Rectangle> partition;
public:
// search the valid dimension that is strictly smaller than 'd'.
uint32_t SearchValidDim(uint32_t d, bool search_width) const {
uint32_t new_d = kMinBlockSizePix;
for (const BlockSize* b = valid_blocks; *b != BLK_LAST; ++b) {
const uint32_t test_d =
search_width ? BlockWidthPix(*b) : BlockHeightPix(*b);
if (test_d > new_d && test_d < d) new_d = test_d;
}
return new_d;
}
void AddBox(const Rectangle& r) {
uint32_t w = std::min(r.width, max_width - r.x);
uint32_t h = std::min(r.height, max_height - r.y);
w = (w + 3u) & ~3u;
h = (h + 3u) & ~3u;
for (const BlockSize* b = valid_blocks; *b != BLK_LAST; ++b) {
if (w == BlockWidthPix(*b) && h == BlockHeightPix(*b)) {
// we found a valid block to push.
partition.emplace_back(r.x, r.y, w, h);
return;
}
}
if (skip_invalid) return;
// We have invalid block to recursively split along the largest dimension.
split_count += 1;
if (w >= h) {
const uint32_t new_w = SearchValidDim(w, /*search_width=*/true);
AddBox({r.x + 0, r.y, new_w, h});
AddBox({r.x + new_w, r.y, w - new_w, h});
} else {
const uint32_t new_h = SearchValidDim(h, /*search_width=*/false);
AddBox({r.x, r.y + 0, w, new_h});
AddBox({r.x, r.y + new_h, w, h - new_h});
}
}
};
bool ReadPartition(const char file_path[], bool read_only,
std::vector<Rectangle>* const partition) {
std::FILE* const file = std::fopen(file_path, "r");
if (file == nullptr) {
// If not 'read_only', it's considered output, not an error.
return !read_only;
}
char line[128];
while (std::fgets(line, sizeof(line), file) != nullptr) {
if (line[0] == '\0' || line[0] == '\n') continue;
Rectangle r;
if (std::sscanf(line, "%u %u %u %u", // NOLINT (sscanf() not recommended)
&r.x, &r.y, &r.width, &r.height) != 4) {
std::fclose(file);
return false; // Parsing failed.
}
partition->emplace_back(r);
}
std::fclose(file);
return true;
}
void ConvertPartition(uint32_t pic_width, uint32_t pic_height,
PartitionSet partition_set, bool ignore_invalid,
std::vector<Rectangle>* const partition) {
PartitionParams p = {GetBlockSizes(partition_set),
pic_width,
pic_height,
ignore_invalid,
/*split_count=*/0,
/*partition=*/{}};
for (Rectangle r : *partition) p.AddBox(r);
if (p.split_count > 0) {
fprintf(stderr, "Number of splits: %u (from %d to %d blocks)\n",
p.split_count, (uint32_t)partition->size(),
(uint32_t)p.partition.size());
}
std::swap(*partition, p.partition);
}
bool WritePartition(const std::vector<Rectangle>& partition,
const char file_path[]) {
std::FILE* const file = std::fopen(file_path, "w");
if (file == nullptr) return false;
for (const Rectangle& rect : partition) {
fprintf(file, "%u %u %u %u\n", rect.x, rect.y, rect.width, rect.height);
}
std::fclose(file);
return true;
}
//------------------------------------------------------------------------------
// Program options.
void ProgramOptions::Add(const char arg[], const std::string& desc) {
content_.emplace_back(arg, desc);
}
void ProgramOptions::Add(const std::string& desc) {
content_.emplace_back(nullptr, desc);
}
void ProgramOptions::AddSystemOptionSection() {
Add("System options:");
// TODO(vrabaud) add("-mt", "use multi-threading");
#ifndef WP2_DLL
Add("-noasm", "disable all assembly optimizations");
#endif
Add("-version", "print version number and exit");
Add("-git", "print git hash if available");
Add("-h / -help", "this help");
Add("");
}
void ProgramOptions::AddMetricOptions() {
Add("-ssim / -ssim_yuv", "print SSIM distortion in RGB / YCoCg");
Add("-msssim", "print MSSSIM distortion in RGB");
Add("-psnr / -psnr_yuv", "print PSNR distortion in RGB / YCoCg");
Add("-lsim", "print LSIM distortion in RGB");
Add("-psnrhvs", "print PSNRHVS distortion");
}
// Writes a string by wrapping it at 80 columns.
static void SafePrint(const char s_in[], uint32_t margin, uint32_t max = 80u) {
margin = std::min(margin, max - 1);
assert(margin < 80 && max <= 80);
// print truncated word-delimited string
while (margin + strlen(s_in) >= max) {
char tmp[80 + 1] = {0};
snprintf(tmp, max - margin, "%s", s_in);
char* end = strrchr(tmp, ' ');
if (end == NULL) end = tmp + max - margin; // word is too long. Ellipsis?
*end = 0;
s_in += end + 1 - tmp;
printf("%s\n%*s", tmp, margin, "");
}
printf("%s\n", s_in);
}
void ProgramOptions::Print() const {
// Figure out the longest argument.
size_t size_max = 0u;
for (const auto& c : content_) {
if (c.first != nullptr) size_max = std::max(size_max, strlen(c.first));
}
// left-justify and print
for (uint32_t i = 0; i < content_.size(); ++i) {
if (content_[i].first == nullptr) { // Print normal text.
printf("%s\n", content_[i].second.c_str());
} else { // Print the arguments.
const size_t padding = strlen(content_[i].first);
printf(" %s %.*s.. ", content_[i].first, (int)(size_max - padding),
"...............................................................");
SafePrint(content_[i].second.c_str(), size_max + 6);
}
}
}
bool ProgramOptions::ParseSystemOptions(const char* const arg,
bool* const must_stop) {
*must_stop = false;
if (!strcmp(arg, "-version")) {
const auto version = (uint32_t)WP2GetVersion();
printf("WebP2 version: %d.%d.%d [%s]\n", (version >> 16u) & 0xffu,
(version >> 8u) & 0xffu, (version >> 0u) & 0xffu,
GitVersion);
*must_stop = true;
return true;
} else if (!strcmp(arg, "-git")) {
printf("%s\n", GitVersion);
*must_stop = true;
return true;
} else if (!strcmp(arg, "-noasm")) {
ExUtilDisableSIMD();
return true;
}
return false;
}
bool ProgramOptions::ParseMetricOptions(const char* const arg,
MetricType* type) {
if (!strcmp(arg, "-psnr")) {
*type = PSNR;
} else if (!strcmp(arg, "-psnrhvs")) {
*type = PSNRHVS;
} else if (!strcmp(arg, "-psnr_yuv")) {
*type = PSNR_YUV;
} else if (!strcmp(arg, "-ssim")) {
*type = SSIM;
} else if (!strcmp(arg, "-ssim_yuv")) {
*type = SSIM_YUV;
} else if (!strcmp(arg, "-msssim")) {
*type = MSSSIM;
} else if (!strcmp(arg, "-lsim")) {
*type = LSIM;
} else {
return false;
}
return true;
}
//------------------------------------------------------------------------------
// Parse numerical values (and update *error in case of error).
int ExUtilGetInt(const char* const v, bool* const error, int base) {
int n;
if (!ExUtilTryGetInt(v, &n, base) && error != nullptr && !*error) {
*error = true;
fprintf(stderr, "Error! '%s' is not an integer.\n",
(v != nullptr) ? v : "(null)");
}
return n;
}
uint32_t ExUtilGetUInt(const char* const v, bool* const error, int base) {
return (uint32_t)ExUtilGetInt(v, error, base);
}
bool ExUtilTryGetInt(const char* const v, int* const n, int base) {
char* end = nullptr;
*n = (v != nullptr) ? (int)strtol(v, &end, base) : 0; // NOLINT
return (end != v);
}
bool ExUtilTryGetUInt(const char* const v, uint32_t* const n, int base) {
char* end = nullptr;
*n = (v != nullptr) ? (uint32_t)strtoul(v, &end, base) : 0; // NOLINT
return (end != v);
}
float ExUtilGetFloat(const char* const v, bool* const error) {
char* end = nullptr;
const float f = (v != nullptr) ? (float)strtod(v, &end) : 0.f;
if (end == v && error != nullptr && !*error) {
*error = true;
fprintf(stderr, "Error! '%s' is not a floating point number.\n",
(v != nullptr) ? v : "(null)");
}
return f;
}
uint32_t ExUtilGetFloats(const char* v, float values[], uint32_t num_values_max,
bool* const error, char separator) {
uint32_t n = 0;
bool parse_error = (v == nullptr);
if (!parse_error) {
while (n < num_values_max && strlen(v) > 0) {
const float value = ExUtilGetFloat(v, &parse_error);
if (parse_error) break;
values[n++] = value;
v = strchr(v, separator);
if (v == NULL) break;
v += 1; // skip the separator
}
}
if (error != nullptr) *error = *error || parse_error;
return parse_error ? 0 : n;
}
//------------------------------------------------------------------------------
constexpr double MultiProgressPrinter::kNumSecondsBetweenPrints;
void MultiProgressPrinter::Init(uint32_t num_jobs) {
jobs_.clear();
jobs_.reserve(num_jobs);
for (uint32_t i = 0; i < num_jobs; ++i) jobs_.emplace_back(this, i);
started_ = false;
num_finished_jobs_ = 0;
}
void MultiProgressPrinter::OnUpdate(bool job_just_finished) {
if (jobs_.size() > 1) WP2_ASSERT_STATUS(lock_.Acquire());
assert(num_finished_jobs_ < jobs_.size());
if (job_just_finished) ++num_finished_jobs_;
if (num_finished_jobs_ == jobs_.size()) {
printf("100.00 %%\n");
} else {
const double timestamp = GetStopwatchTime();
if (!started_ || (timestamp - last_timestamp_) > kNumSecondsBetweenPrints) {
double sum = 0;
for (const Job& job : jobs_) sum += job.progress_;
printf("%6.2f %%\n", sum * 100. / jobs_.size());
started_ = true;
last_timestamp_ = timestamp;
}
}
if (jobs_.size() > 1) lock_.Release();
}
bool MultiProgressPrinter::Job::OnUpdate(double progress) {
if (!finished_) {
progress_ = progress;
if (progress_ > 0.999999) {
progress_ = 1.;
finished_ = true;
}
parent_->OnUpdate(finished_);
}
return true;
}
//------------------------------------------------------------------------------
} // namespace WP2