322 lines
12 KiB
C++
322 lines
12 KiB
C++
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
#include "tools/djxl.h"
|
|
|
|
#include <stdio.h>
|
|
|
|
#include "lib/extras/codec.h"
|
|
#include "lib/extras/codec_jpg.h"
|
|
#include "lib/extras/color_description.h"
|
|
#include "lib/extras/time.h"
|
|
#include "lib/extras/tone_mapping.h"
|
|
#include "lib/jxl/alpha.h"
|
|
#include "lib/jxl/base/data_parallel.h"
|
|
#include "lib/jxl/base/file_io.h"
|
|
#include "lib/jxl/base/override.h"
|
|
#include "lib/jxl/color_encoding_internal.h"
|
|
#include "lib/jxl/color_management.h"
|
|
#include "lib/jxl/dec_file.h"
|
|
#include "lib/jxl/frame_header.h"
|
|
#include "lib/jxl/image.h"
|
|
#include "lib/jxl/image_bundle.h"
|
|
#include "lib/jxl/image_ops.h"
|
|
#include "tools/args.h"
|
|
#include "tools/box/box.h"
|
|
#include "tools/cpu/cpu.h"
|
|
|
|
namespace jpegxl {
|
|
namespace tools {
|
|
|
|
static inline bool ParseLuminanceRange(const char* arg,
|
|
std::pair<float, float>* out) {
|
|
char* end;
|
|
out->first = static_cast<float>(strtod(arg, &end));
|
|
if (*end == '\0') {
|
|
// That was actually the upper bound.
|
|
out->second = out->first;
|
|
out->first = 0;
|
|
return true;
|
|
}
|
|
if (*end != '-') {
|
|
fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg);
|
|
return JXL_FAILURE("Args");
|
|
}
|
|
const char* second = end + 1;
|
|
out->second = static_cast<float>(strtod(second, &end));
|
|
if (*end != '\0') {
|
|
fprintf(stderr, "Unable to interpret as luminance range: %s.\n", arg);
|
|
return JXL_FAILURE("Args");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DecompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) {
|
|
// Positional arguments.
|
|
cmdline->AddPositionalOption("INPUT", /* required = */ true,
|
|
"the compressed input file", &file_in);
|
|
|
|
cmdline->AddPositionalOption(
|
|
"OUTPUT", /* required = */ true,
|
|
"the output can be PNG with ICC, JPG, or PPM/PFM.", &file_out);
|
|
|
|
cmdline->AddOptionFlag('V', "version", "print version number and exit",
|
|
&version, &SetBooleanTrue);
|
|
|
|
cmdline->AddOptionValue('\0', "num_reps", "N", nullptr, &num_reps,
|
|
&ParseUnsigned);
|
|
|
|
opt_num_threads_id = cmdline->AddOptionValue('\0', "num_threads", "N",
|
|
"The number of threads to use",
|
|
&num_threads, &ParseUnsigned);
|
|
|
|
cmdline->AddOptionValue('\0', "print_profile", "0|1",
|
|
"print timing information before exiting",
|
|
&print_profile, &ParseOverride);
|
|
|
|
cmdline->AddOptionValue('\0', "bits_per_sample", "N",
|
|
"defaults to original (input) bit depth",
|
|
&bits_per_sample, &ParseUnsigned);
|
|
|
|
cmdline->AddOptionFlag(
|
|
'\0', "tone_map",
|
|
"tone map the image to the luminance range indicated by --display_nits "
|
|
"instead of performing a naive 0-1 -> 0-1 conversion",
|
|
&tone_map, &SetBooleanTrue);
|
|
|
|
cmdline->AddOptionValue('\0', "display_nits", "0.3-250",
|
|
"luminance range of the display to which to "
|
|
"tone-map; the lower bound can be omitted",
|
|
&display_nits, &ParseLuminanceRange);
|
|
|
|
cmdline->AddOptionValue('\0', "color_space", "RGB_D65_SRG_Rel_Lin",
|
|
"defaults to original (input) color space",
|
|
&color_space, &ParseString);
|
|
|
|
cmdline->AddOptionValue('s', "downsampling", "1,2,4,8,16",
|
|
"maximum permissible downsampling factor (values "
|
|
"greater than 16 will return the LQIP if available)",
|
|
¶ms.max_downsampling, &ParseUnsigned);
|
|
|
|
cmdline->AddOptionFlag('\0', "allow_partial_files",
|
|
"allow decoding of truncated files",
|
|
¶ms.allow_partial_files, &SetBooleanTrue);
|
|
|
|
cmdline->AddOptionFlag('\0', "allow_more_progressive_steps",
|
|
"allow decoding more progressive steps in truncated "
|
|
"files. No effect without --allow_partial_files",
|
|
¶ms.allow_more_progressive_steps, &SetBooleanTrue);
|
|
|
|
#if JPEGXL_ENABLE_JPEG
|
|
cmdline->AddOptionFlag(
|
|
'j', "pixels_to_jpeg",
|
|
"By default, if the input JPEG XL contains a recompressed JPEG file, "
|
|
"djxl "
|
|
"reconstructs the exact original JPEG file. This flag causes the decoder "
|
|
"to instead decode the image to pixels and encode a new (lossy) JPEG. "
|
|
"The output file if provided must be a .jpg or .jpeg file.",
|
|
&decode_to_pixels, &SetBooleanTrue);
|
|
|
|
opt_jpeg_quality_id =
|
|
cmdline->AddOptionValue('q', "jpeg_quality", "N",
|
|
"JPEG output quality. Setting an output quality "
|
|
"implies --pixels_to_jpeg.",
|
|
&jpeg_quality, &ParseUnsigned);
|
|
#endif
|
|
|
|
#if JPEGXL_ENABLE_SJPEG
|
|
cmdline->AddOptionFlag('\0', "use_sjpeg",
|
|
"use sjpeg instead of libjpeg for JPEG output",
|
|
&use_sjpeg, &SetBooleanTrue);
|
|
#endif
|
|
|
|
cmdline->AddOptionFlag('\0', "print_read_bytes",
|
|
"print total number of decoded bytes",
|
|
&print_read_bytes, &SetBooleanTrue);
|
|
|
|
cmdline->AddOptionFlag('\0', "quiet", "silence output (except for errors)",
|
|
&quiet, &SetBooleanTrue);
|
|
}
|
|
|
|
jxl::Status DecompressArgs::ValidateArgs(const CommandLineParser& cmdline) {
|
|
if (file_in == nullptr) {
|
|
fprintf(stderr, "Missing INPUT filename.\n");
|
|
return false;
|
|
}
|
|
|
|
// User didn't override num_threads, so we have to compute a default, which
|
|
// might fail, so only do so when necessary. Don't just check num_threads != 0
|
|
// because the user may have set it to that.
|
|
if (!cmdline.GetOption(opt_num_threads_id)->matched()) {
|
|
cpu::ProcessorTopology topology;
|
|
if (!cpu::DetectProcessorTopology(&topology)) {
|
|
// We have seen sporadic failures caused by setaffinity_np.
|
|
fprintf(stderr,
|
|
"Failed to choose default num_threads; you can avoid this "
|
|
"error by specifying a --num_threads N argument.\n");
|
|
return false;
|
|
}
|
|
num_threads = topology.packages * topology.cores_per_package;
|
|
}
|
|
|
|
#if JPEGXL_ENABLE_JPEG
|
|
if (cmdline.GetOption(opt_jpeg_quality_id)->matched()) {
|
|
decode_to_pixels = true;
|
|
}
|
|
#endif
|
|
if (file_out) {
|
|
const std::string extension = jxl::Extension(file_out);
|
|
const jxl::Codec codec =
|
|
jxl::CodecFromExtension(extension, &bits_per_sample);
|
|
if (codec != jxl::Codec::kJPG) {
|
|
// when decoding to anything-but-JPEG, we'll need pixels
|
|
decode_to_pixels = true;
|
|
}
|
|
} else {
|
|
decode_to_pixels = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
jxl::Status DecompressJxlToPixels(const jxl::Span<const uint8_t> compressed,
|
|
const jxl::DecompressParams& params,
|
|
jxl::ThreadPool* pool,
|
|
jxl::CodecInOut* JXL_RESTRICT io,
|
|
SpeedStats* JXL_RESTRICT stats) {
|
|
const double t0 = jxl::Now();
|
|
if (!jxl::DecodeFile(params, compressed, io, pool)) {
|
|
fprintf(stderr, "Failed to decompress to pixels.\n");
|
|
return false;
|
|
}
|
|
const double t1 = jxl::Now();
|
|
stats->NotifyElapsed(t1 - t0);
|
|
stats->SetImageSize(io->xsize(), io->ysize());
|
|
return true;
|
|
}
|
|
|
|
jxl::Status DecompressJxlToJPEG(const JpegXlContainer& container,
|
|
const DecompressArgs& args,
|
|
jxl::ThreadPool* pool, jxl::PaddedBytes* output,
|
|
SpeedStats* JXL_RESTRICT stats) {
|
|
output->clear();
|
|
const double t0 = jxl::Now();
|
|
|
|
jxl::Span<const uint8_t> compressed(container.codestream,
|
|
container.codestream_size);
|
|
|
|
JXL_RETURN_IF_ERROR(compressed.size() >= 2);
|
|
|
|
// JXL case
|
|
// Decode to DCT when possible and generate a JPG file.
|
|
jxl::CodecInOut io;
|
|
// Set JPEG quality.
|
|
// TODO(deymo): We should probably fail to give a JPEG file if the
|
|
// original image can't be transcoded to a JPEG file without passing
|
|
// through pixels, or at least signal this to the user.
|
|
io.use_sjpeg = args.use_sjpeg;
|
|
io.jpeg_quality = args.jpeg_quality;
|
|
|
|
if (!DecodeJpegXlToJpeg(args.params, container, &io, pool)) {
|
|
return JXL_FAILURE("Failed to decode JXL to JPEG");
|
|
}
|
|
if (!jxl::extras::EncodeImageJPGCoefficients(&io, output)) {
|
|
return JXL_FAILURE("Failed to generate JPEG");
|
|
}
|
|
stats->SetImageSize(io.xsize(), io.ysize());
|
|
|
|
const double t1 = jxl::Now();
|
|
stats->NotifyElapsed(t1 - t0);
|
|
stats->SetFileSize(output->size());
|
|
return true;
|
|
}
|
|
|
|
jxl::Status WriteJxlOutput(const DecompressArgs& args, const char* file_out,
|
|
jxl::CodecInOut& io, jxl::ThreadPool* pool) {
|
|
// Can only write if we decoded and have an output filename.
|
|
// (Writing large PNGs is slow, so allow skipping it for benchmarks.)
|
|
if (file_out == nullptr) return true;
|
|
|
|
// Override original color space with arg if specified.
|
|
jxl::ColorEncoding c_out = io.metadata.m.color_encoding;
|
|
if (!args.color_space.empty()) {
|
|
bool color_space_applied = false;
|
|
JxlColorEncoding c_out_external;
|
|
if (jxl::ParseDescription(args.color_space, &c_out_external) &&
|
|
ConvertExternalToInternalColorEncoding(c_out_external, &c_out) &&
|
|
c_out.CreateICC()) {
|
|
color_space_applied = true;
|
|
} else {
|
|
jxl::PaddedBytes icc;
|
|
if (jxl::ReadFile(args.color_space, &icc) &&
|
|
c_out.SetICC(std::move(icc))) {
|
|
color_space_applied = true;
|
|
}
|
|
}
|
|
|
|
if (!color_space_applied) {
|
|
fprintf(stderr, "Failed to apply color_space.\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Override original #bits with arg if specified.
|
|
size_t bits_per_sample = io.metadata.m.bit_depth.bits_per_sample;
|
|
if (args.bits_per_sample != 0) bits_per_sample = args.bits_per_sample;
|
|
|
|
if (args.tone_map) {
|
|
jxl::Status status = jxl::ToneMapTo(args.display_nits, &io, pool);
|
|
if (!status) fprintf(stderr, "Failed to map tones.\n");
|
|
JXL_RETURN_IF_ERROR(status);
|
|
if (c_out.tf.IsPQ() && args.color_space.empty()) {
|
|
// Prevent writing the tone-mapped image to PQ output unless explicitly
|
|
// requested. The result would look even dimmer than it would have without
|
|
// tone mapping.
|
|
c_out.tf.SetTransferFunction(jxl::TransferFunction::kSRGB);
|
|
status = c_out.CreateICC();
|
|
if (!status) fprintf(stderr, "Failed to create ICC\n");
|
|
JXL_RETURN_IF_ERROR(c_out.CreateICC());
|
|
}
|
|
}
|
|
|
|
if (!io.metadata.m.have_animation) {
|
|
if (!EncodeToFile(io, c_out, bits_per_sample, file_out, pool)) {
|
|
fprintf(stderr, "Failed to write decoded image.\n");
|
|
return false;
|
|
}
|
|
} else {
|
|
const char* extension = strrchr(file_out, '.');
|
|
std::string base = extension == nullptr
|
|
? std::string(file_out)
|
|
: std::string(file_out, extension - file_out);
|
|
if (extension == nullptr) extension = "";
|
|
const int digits = 1 + static_cast<int>(std::log10(std::max(
|
|
1, static_cast<int>(io.frames.size() - 1))));
|
|
std::vector<char> output_filename;
|
|
output_filename.resize(base.size() + 1 + digits + strlen(extension) + 1);
|
|
|
|
for (size_t i = 0; i < io.frames.size(); ++i) {
|
|
jxl::CodecInOut frame_io;
|
|
frame_io.SetFromImage(jxl::CopyImage(*io.frames[i].color()),
|
|
io.frames[i].c_current());
|
|
frame_io.metadata.m = *io.frames[i].metadata();
|
|
if (io.frames[i].HasAlpha()) {
|
|
frame_io.Main().SetAlpha(
|
|
jxl::CopyImage(*io.frames[i].alpha()),
|
|
/*alpha_is_premultiplied=*/io.frames[i].AlphaIsPremultiplied());
|
|
}
|
|
snprintf(output_filename.data(), output_filename.size(), "%s-%0*zu%s",
|
|
base.c_str(), digits, i, extension);
|
|
if (!EncodeToFile(frame_io, c_out, bits_per_sample,
|
|
output_filename.data(), pool)) {
|
|
fprintf(stderr, "Failed to write decoded image for frame %zu/%zu.\n",
|
|
i + 1, io.frames.size());
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace tools
|
|
} // namespace jpegxl
|