blob: 19b3c2fc037355725bc88d202361d11e10af860e [file]
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
#include "tonemap_command.h"
#include <cmath>
#include "avif/avif_cxx.h"
#include "imageio.h"
namespace avif {
TonemapCommand::TonemapCommand()
: ProgramCommand("tonemap",
"Tone maps an avif image that has a gain map to a "
"given HDR headroom (how much brighter the display can go "
"compared to an SDR display)") {
argparse_.add_argument(arg_input_filename_, "input_image");
argparse_.add_argument(arg_output_filename_, "output_image");
argparse_.add_argument(arg_headroom_, "--headroom")
.help(
"HDR headroom to tone map to. This is log2 of the ratio of HDR to "
"SDR luminance. 0 means SDR.")
.default_value("0");
argparse_
.add_argument<CicpValues, CicpConverter>(arg_input_cicp_, "--cicp-input")
.help(
"Override input CICP values, expressed as P/T/M "
"where P = color primaries, T = transfer characteristics, "
"M = matrix coefficients.");
argparse_
.add_argument<CicpValues, CicpConverter>(arg_output_cicp_,
"--cicp-output")
.help(
"CICP values for the output, expressed as P/T/M "
"where P = color primaries, T = transfer characteristics, "
"M = matrix coefficients. P and M are only relevant when saving to "
"AVIF. "
"If not specified, 'color primaries' defaults to the base image's "
"primaries, 'transfer characteristics' defaults to 16 (PQ) if "
"headroom > 0, or 13 (sRGB) otherwise, 'matrix coefficients' "
"defaults to 6 (BT601).");
argparse_.add_argument(arg_clli_str_, "--clli")
.help(
"Override content light level information expressed as: "
"MaxCLL,MaxPALL. Only relevant when saving to AVIF.");
arg_image_read_.Init(argparse_);
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/true);
}
avifResult TonemapCommand::Run() {
avifContentLightLevelInformationBox clli_box = {};
bool clli_set = false;
if (arg_clli_str_.value().size() > 0) {
std::vector<uint32_t> clli;
if (!ParseList(arg_clli_str_, ',', 2, &clli)) {
std::cerr << "Invalid clli values, expected format: maxCLL,maxPALL where "
"both maxCLL and maxPALL are positive integers, got: "
<< arg_clli_str_ << "\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}
clli_box.maxCLL = clli[0];
clli_box.maxPALL = clli[1];
clli_set = true;
}
const float headroom = arg_headroom_;
const bool tone_mapping_to_sdr = (headroom == 0.0f);
CicpValues cicp;
if (arg_output_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
cicp = arg_output_cicp_;
} else {
const avifTransferCharacteristics transfer_characteristics =
tone_mapping_to_sdr ? AVIF_TRANSFER_CHARACTERISTICS_SRGB
: AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084;
cicp = {AVIF_COLOR_PRIMARIES_UNKNOWN, transfer_characteristics,
AVIF_MATRIX_COEFFICIENTS_BT601};
}
DecoderPtr decoder(avifDecoderCreate());
if (decoder == NULL) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
arg_image_read_.ignore_profile);
if (result != AVIF_RESULT_OK) {
return result;
}
if (arg_input_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
decoder->image->colorPrimaries = arg_input_cicp_.value().color_primaries;
decoder->image->transferCharacteristics =
arg_input_cicp_.value().transfer_characteristics;
decoder->image->matrixCoefficients =
arg_input_cicp_.value().matrix_coefficients;
}
if (decoder->image->gainMap.image == nullptr) {
std::cerr << "Input image " << arg_input_filename_
<< " does not contain a gain map\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}
avifGainMapMetadataDouble metadata;
if (!avifGainMapMetadataFractionsToDouble(
&metadata, &decoder->image->gainMap.metadata)) {
std::cerr << "Input image " << arg_input_filename_
<< " has invalid gain map metadata\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}
if (!clli_set) {
// Use the clli from the base image or the alternate image if the headroom
// is outside of the [baseHdrHeadroom, alternateHdrHeadroom] range.
if ((headroom <= metadata.baseHdrHeadroom &&
metadata.baseHdrHeadroom <= metadata.alternateHdrHeadroom) ||
(headroom >= metadata.baseHdrHeadroom &&
metadata.baseHdrHeadroom >= metadata.alternateHdrHeadroom)) {
clli_box = decoder->image->clli;
} else if ((headroom <= metadata.alternateHdrHeadroom &&
metadata.alternateHdrHeadroom <= metadata.baseHdrHeadroom) ||
(headroom >= metadata.alternateHdrHeadroom &&
metadata.alternateHdrHeadroom >= metadata.baseHdrHeadroom)) {
clli_box = decoder->image->gainMap.image->clli;
}
clli_set = (clli_box.maxCLL != 0) || (clli_box.maxPALL != 0);
}
int depth = arg_image_read_.depth;
if (depth == 0) {
depth = tone_mapping_to_sdr ? 8 : decoder->image->depth;
}
ImagePtr tone_mapped(
avifImageCreate(decoder->image->width, decoder->image->height, depth,
(avifPixelFormat)arg_image_read_.pixel_format.value()));
if (tone_mapped == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
avifRGBImage tone_mapped_rgb;
avifRGBImageSetDefaults(&tone_mapped_rgb, tone_mapped.get());
avifDiagnostics diag;
result = avifImageApplyGainMap(decoder->image, &decoder->image->gainMap,
arg_headroom_, cicp.transfer_characteristics,
&tone_mapped_rgb,
clli_set ? nullptr : &clli_box, &diag);
if (result != AVIF_RESULT_OK) {
std::cout << "Failed to tone map image: " << avifResultToString(result)
<< " (" << diag.error << ")\n";
return result;
}
result = avifImageRGBToYUV(tone_mapped.get(), &tone_mapped_rgb);
if (result != AVIF_RESULT_OK) {
std::cerr << "Failed to convert to YUV: " << avifResultToString(result)
<< "\n";
return result;
}
if (cicp.color_primaries == AVIF_COLOR_PRIMARIES_UNKNOWN) {
// TODO(maryla): for now avifImageApplyGainMap always uses the primaries of
// the base image, but it should take into account the metadata's
// useBaseColorSpace property.
cicp.color_primaries = decoder->image->colorPrimaries;
}
tone_mapped->clli = clli_box;
tone_mapped->transferCharacteristics = cicp.transfer_characteristics;
tone_mapped->colorPrimaries = cicp.color_primaries;
tone_mapped->matrixCoefficients = cicp.matrix_coefficients;
return WriteImage(tone_mapped.get(), arg_output_filename_,
arg_image_encode_.quality, arg_image_encode_.speed);
}
} // namespace avif