// 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 #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* out) { char* end; out->first = static_cast(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(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 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 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(std::log10(std::max( 1, static_cast(io.frames.size() - 1)))); std::vector 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