summaryrefslogtreecommitdiff
path: root/tools/hdr
diff options
context:
space:
mode:
Diffstat (limited to 'tools/hdr')
-rw-r--r--tools/hdr/README.md16
-rw-r--r--tools/hdr/display_to_hlg.cc25
-rw-r--r--tools/hdr/exr_to_pq.cc158
-rw-r--r--tools/hdr/generate_lut_template.cc11
-rw-r--r--tools/hdr/image_utils.h35
-rw-r--r--tools/hdr/local_tone_map.cc541
-rw-r--r--tools/hdr/pq_to_hlg.cc23
-rw-r--r--tools/hdr/render_hlg.cc23
-rw-r--r--tools/hdr/texture_to_cube.cc11
-rw-r--r--tools/hdr/tone_map.cc28
10 files changed, 823 insertions, 48 deletions
diff --git a/tools/hdr/README.md b/tools/hdr/README.md
index 227b22b..85eb1bd 100644
--- a/tools/hdr/README.md
+++ b/tools/hdr/README.md
@@ -99,6 +99,22 @@ This is the mathematical inverse of `tools/render_hlg`. Furthermore,
`tools/pq_to_hlg` is equivalent to `tools/tone_map -t 1000` followed by
`tools/display_to_hlg -m 1000`.
+## OpenEXR to PQ
+
+`tools/exr_to_pq` converts an OpenEXR image into a Rec. 2020 + PQ image, which
+can be saved as a PNG or PPM file. Luminance information is taken from the
+`whiteLuminance` tag if the input has it, and otherwise defaults to treating
+(1, 1, 1) as 100 cd/m². It is also possible to override this using the
+`--luminance` (`-l`) flag, in two different ways:
+
+```shell
+# Specifies that the brightest pixel in the image happens to be 1500 cd/m².
+$ tools/exr_to_pq --luminance='max=1500' input.exr output.png
+
+# Specifies that (1, 1, 1) in the input file is 203 cd/m².
+$ tools/exr_to_pq --luminance='white=203' input.exr output.png
+```
+
# LUT generation
There are additionally two tools that can be used to generate look-up tables
diff --git a/tools/hdr/display_to_hlg.cc b/tools/hdr/display_to_hlg.cc
index a2caef2..8fa8fde 100644
--- a/tools/hdr/display_to_hlg.cc
+++ b/tools/hdr/display_to_hlg.cc
@@ -9,13 +9,15 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
+#include "lib/jxl/base/span.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -64,9 +66,11 @@ int main(int argc, const char** argv) {
return EXIT_FAILURE;
}
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
jxl::CodecInOut image;
- JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
- &pool));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &image, &pool));
image.metadata.m.SetIntensityTarget(max_nits);
JXL_CHECK(jxl::HlgInverseOOTF(
&image.Main(), jxl::GetHlgGamma(max_nits, surround_nits), &pool));
@@ -75,11 +79,12 @@ int main(int argc, const char** argv) {
jxl::ColorEncoding hlg;
hlg.SetColorSpace(jxl::ColorSpace::kRGB);
- hlg.primaries = jxl::Primaries::k2100;
- hlg.white_point = jxl::WhitePoint::kD65;
- hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.SetPrimariesType(jxl::Primaries::k2100));
+ JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65));
+ hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG);
JXL_CHECK(hlg.CreateICC());
- JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool));
image.metadata.m.color_encoding = hlg;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/exr_to_pq.cc b/tools/hdr/exr_to_pq.cc
new file mode 100644
index 0000000..c7ce1b7
--- /dev/null
+++ b/tools/hdr/exr_to_pq.cc
@@ -0,0 +1,158 @@
+// 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 <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/dec/decode.h"
+#include "lib/extras/packed_image_convert.h"
+#include "lib/jxl/cms/jxl_cms_internal.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
+
+namespace {
+
+struct LuminanceInfo {
+ enum class Kind { kWhite, kMaximum };
+ Kind kind = Kind::kWhite;
+ float luminance = 100.f;
+};
+
+bool ParseLuminanceInfo(const char* argument, LuminanceInfo* luminance_info) {
+ if (strncmp(argument, "white=", 6) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kWhite;
+ argument += 6;
+ } else if (strncmp(argument, "max=", 4) == 0) {
+ luminance_info->kind = LuminanceInfo::Kind::kMaximum;
+ argument += 4;
+ } else {
+ fprintf(stderr,
+ "Invalid prefix for luminance info, expected white= or max=\n");
+ return false;
+ }
+ return jpegxl::tools::ParseFloat(argument, &luminance_info->luminance);
+}
+
+} // namespace
+
+int main(int argc, const char** argv) {
+ jpegxl::tools::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ LuminanceInfo luminance_info;
+ auto luminance_option =
+ parser.AddOptionValue('l', "luminance", "<max|white=N>",
+ "luminance information (defaults to whiteLuminance "
+ "header if present, otherwise to white=100)",
+ &luminance_info, &ParseLuminanceInfo, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::extras::PackedPixelFile ppf;
+ std::vector<uint8_t> input_bytes;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &input_bytes));
+ JXL_CHECK(jxl::extras::DecodeBytes(jxl::Bytes(input_bytes),
+ jxl::extras::ColorHints(), &ppf));
+
+ jxl::CodecInOut image;
+ JXL_CHECK(
+ jxl::extras::ConvertPackedPixelFileToCodecInOut(ppf, &pool, &image));
+ image.metadata.m.bit_depth.exponent_bits_per_sample = 0;
+ jxl::ColorEncoding linear_rec_2020 = image.Main().c_current();
+ JXL_CHECK(linear_rec_2020.SetPrimariesType(jxl::Primaries::k2100));
+ linear_rec_2020.Tf().SetTransferFunction(jxl::TransferFunction::kLinear);
+ JXL_CHECK(linear_rec_2020.CreateICC());
+ JXL_CHECK(
+ jpegxl::tools::TransformCodecInOutTo(image, linear_rec_2020, &pool));
+
+ float primaries_xyz[9];
+ const jxl::PrimariesCIExy p = image.Main().c_current().GetPrimaries();
+ const jxl::CIExy wp = image.Main().c_current().GetWhitePoint();
+ JXL_CHECK(jxl::PrimariesToXYZ(p.r.x, p.r.y, p.g.x, p.g.y, p.b.x, p.b.y, wp.x,
+ wp.y, primaries_xyz));
+
+ float max_value = 0.f;
+ float max_relative_luminance = 0.f;
+ float white_luminance = ppf.info.intensity_target != 0 &&
+ !parser.GetOption(luminance_option)->matched()
+ ? ppf.info.intensity_target
+ : luminance_info.kind == LuminanceInfo::Kind::kWhite
+ ? luminance_info.luminance
+ : 0.f;
+ bool out_of_gamut = false;
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const rows[3] = {image.Main().color()->ConstPlaneRow(0, y),
+ image.Main().color()->ConstPlaneRow(1, y),
+ image.Main().color()->ConstPlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ if (!out_of_gamut &&
+ (rows[0][x] < 0 || rows[1][x] < 0 || rows[2][x] < 0)) {
+ out_of_gamut = true;
+ fprintf(stderr,
+ "WARNING: found colors outside of the Rec. 2020 gamut.\n");
+ }
+ max_value = std::max(
+ max_value, std::max(rows[0][x], std::max(rows[1][x], rows[2][x])));
+ const float luminance = primaries_xyz[1] * rows[0][x] +
+ primaries_xyz[4] * rows[1][x] +
+ primaries_xyz[7] * rows[2][x];
+ if (luminance_info.kind == LuminanceInfo::Kind::kMaximum &&
+ luminance > max_relative_luminance) {
+ max_relative_luminance = luminance;
+ white_luminance = luminance_info.luminance / luminance;
+ }
+ }
+ }
+ jxl::ScaleImage(1.f / max_value, image.Main().color());
+ white_luminance *= max_value;
+ image.metadata.m.SetIntensityTarget(white_luminance);
+ if (white_luminance > 10000) {
+ fprintf(stderr,
+ "WARNING: the image is too bright for PQ (would need (1, 1, 1) to "
+ "be %g cd/m^2).\n",
+ white_luminance);
+ } else {
+ fprintf(stderr,
+ "The resulting image should be compressed with "
+ "--intensity_target=%g.\n",
+ white_luminance);
+ }
+
+ jxl::ColorEncoding pq = image.Main().c_current();
+ pq.Tf().SetTransferFunction(jxl::TransferFunction::kPQ);
+ JXL_CHECK(pq.CreateICC());
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, pq, &pool));
+ image.metadata.m.color_encoding = pq;
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
+}
diff --git a/tools/hdr/generate_lut_template.cc b/tools/hdr/generate_lut_template.cc
index 626d54f..da8ecee 100644
--- a/tools/hdr/generate_lut_template.cc
+++ b/tools/hdr/generate_lut_template.cc
@@ -7,12 +7,13 @@
#include <stdlib.h>
#include "lib/extras/codec.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/image_metadata.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
size_t N = 64;
@@ -55,6 +56,8 @@ int main(int argc, const char** argv) {
jxl::CodecInOut output;
output.metadata.m.bit_depth.bits_per_sample = 16;
output.SetFromImage(std::move(image), jxl::ColorEncoding::SRGB());
- JXL_CHECK(jxl::EncodeToFile(output, jxl::ColorEncoding::SRGB(), 16,
- output_filename, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jxl::Encode(output, jxl::ColorEncoding::SRGB(), 16, output_filename,
+ &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/image_utils.h b/tools/hdr/image_utils.h
new file mode 100644
index 0000000..901c2b6
--- /dev/null
+++ b/tools/hdr/image_utils.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef TOOLS_HDR_IMAGE_UTILS_H_
+#define TOOLS_HDR_IMAGE_UTILS_H_
+
+#include <jxl/cms.h>
+#include <jxl/cms_interface.h>
+
+#include "lib/jxl/base/status.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/image_bundle.h"
+
+namespace jpegxl {
+namespace tools {
+
+static inline jxl::Status TransformCodecInOutTo(
+ jxl::CodecInOut& io, const jxl::ColorEncoding& c_desired,
+ jxl::ThreadPool* pool) {
+ const JxlCmsInterface& cms = *JxlGetDefaultCms();
+ if (io.metadata.m.have_preview) {
+ JXL_RETURN_IF_ERROR(io.preview_frame.TransformTo(c_desired, cms, pool));
+ }
+ for (jxl::ImageBundle& ib : io.frames) {
+ JXL_RETURN_IF_ERROR(ib.TransformTo(c_desired, cms, pool));
+ }
+ return true;
+}
+
+} // namespace tools
+} // namespace jpegxl
+
+#endif // TOOLS_HDR_IMAGE_UTILS_H_
diff --git a/tools/hdr/local_tone_map.cc b/tools/hdr/local_tone_map.cc
new file mode 100644
index 0000000..b6582a6
--- /dev/null
+++ b/tools/hdr/local_tone_map.cc
@@ -0,0 +1,541 @@
+// 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 <jxl/cms.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lib/extras/codec.h"
+#include "lib/extras/tone_mapping.h"
+#include "lib/jxl/convolve.h"
+#include "lib/jxl/enc_gamma_correct.h"
+#include "lib/jxl/image_bundle.h"
+#include "tools/args.h"
+#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
+
+namespace jxl {
+namespace {
+
+constexpr WeightsSeparable5 kPyramidFilter = {
+ {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)},
+ {HWY_REP4(.375f), HWY_REP4(.25f), HWY_REP4(.0625f)}};
+
+template <typename Tin, typename Tout>
+void Subtract(const Image3<Tin>& image1, const Image3<Tin>& image2,
+ Image3<Tout>* out) {
+ const size_t xsize = image1.xsize();
+ const size_t ysize = image1.ysize();
+ JXL_CHECK(xsize == image2.xsize());
+ JXL_CHECK(ysize == image2.ysize());
+
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < ysize; ++y) {
+ const Tin* const JXL_RESTRICT row1 = image1.ConstPlaneRow(c, y);
+ const Tin* const JXL_RESTRICT row2 = image2.ConstPlaneRow(c, y);
+ Tout* const JXL_RESTRICT row_out = out->PlaneRow(c, y);
+ for (size_t x = 0; x < xsize; ++x) {
+ row_out[x] = row1[x] - row2[x];
+ }
+ }
+ }
+}
+
+// Adds `what` of the size of `rect` to `to` in the position of `rect`.
+template <typename Tin, typename Tout>
+void AddTo(const Rect& rect, const Image3<Tin>& what, Image3<Tout>* to) {
+ const size_t xsize = what.xsize();
+ const size_t ysize = what.ysize();
+ JXL_ASSERT(xsize == rect.xsize());
+ JXL_ASSERT(ysize == rect.ysize());
+ for (size_t c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < ysize; ++y) {
+ const Tin* JXL_RESTRICT row_what = what.ConstPlaneRow(c, y);
+ Tout* JXL_RESTRICT row_to = rect.PlaneRow(to, c, y);
+ for (size_t x = 0; x < xsize; ++x) {
+ row_to[x] += row_what[x];
+ }
+ }
+ }
+}
+
+template <typename T>
+Plane<T> Product(const Plane<T>& a, const Plane<T>& b) {
+ Plane<T> c(a.xsize(), a.ysize());
+ for (size_t y = 0; y < a.ysize(); ++y) {
+ const T* const JXL_RESTRICT row_a = a.Row(y);
+ const T* const JXL_RESTRICT row_b = b.Row(y);
+ T* const JXL_RESTRICT row_c = c.Row(y);
+ for (size_t x = 0; x < a.xsize(); ++x) {
+ row_c[x] = row_a[x] * row_b[x];
+ }
+ }
+ return c;
+}
+
+// Expects sRGB input.
+// Will call consumer(x, y, contrast) for each pixel.
+template <typename Consumer>
+void Contrast(const jxl::Image3F& image, const Consumer& consumer,
+ ThreadPool* const pool) {
+ static constexpr WeightsSymmetric3 kLaplacianWeights = {
+ {HWY_REP4(-4)}, {HWY_REP4(1)}, {HWY_REP4(0)}};
+ ImageF grayscale(image.xsize(), image.ysize());
+ static constexpr float kLuminances[3] = {0.2126, 0.7152, 0.0722};
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT input_rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ float* const JXL_RESTRICT row = grayscale.Row(y);
+
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ row[x] = LinearToSrgb8Direct(
+ kLuminances[0] * Srgb8ToLinearDirect(input_rows[0][x]) +
+ kLuminances[1] * Srgb8ToLinearDirect(input_rows[1][x]) +
+ kLuminances[2] * Srgb8ToLinearDirect(input_rows[2][x]));
+ }
+ }
+
+ ImageF laplacian(image.xsize(), image.ysize());
+ Symmetric3(grayscale, Rect(grayscale), kLaplacianWeights, pool, &laplacian);
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT row = laplacian.ConstRow(y);
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ consumer(x, y, std::abs(row[x]));
+ }
+ }
+}
+
+template <typename Consumer>
+void Saturation(const jxl::Image3F& image, const Consumer& consumer) {
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ // TODO(sboukortt): experiment with other methods of computing the
+ // saturation, e.g. C*/L* in LUV/LCh.
+ const float mean = (1.f / 3) * (rows[0][x] + rows[1][x] + rows[2][x]);
+ const float deviations[3] = {rows[0][x] - mean, rows[1][x] - mean,
+ rows[2][x] - mean};
+ consumer(x, y,
+ std::sqrt((1.f / 3) * (deviations[0] * deviations[0] +
+ deviations[1] * deviations[1] +
+ deviations[2] * deviations[2])));
+ }
+ }
+}
+
+template <typename Consumer>
+void MidToneness(const jxl::Image3F& image, const float sigma,
+ const Consumer& consumer) {
+ const float inv_sigma_squared = 1.f / (sigma * sigma);
+ const auto Gaussian = [inv_sigma_squared](const float x) {
+ return std::exp(-.5f * (x - .5f) * (x - .5f) * inv_sigma_squared);
+ };
+ for (size_t y = 0; y < image.ysize(); ++y) {
+ const float* const JXL_RESTRICT rows[3] = {
+ image.PlaneRow(0, y), image.PlaneRow(1, y), image.PlaneRow(2, y)};
+ for (size_t x = 0; x < image.xsize(); ++x) {
+ consumer(
+ x, y,
+ Gaussian(rows[0][x]) * Gaussian(rows[1][x]) * Gaussian(rows[2][x]));
+ }
+ }
+}
+
+ImageF ComputeWeights(const jxl::Image3F& image, const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma, ThreadPool* const pool) {
+ ImageF log_weights(image.xsize(), image.ysize());
+ ZeroFillImage(&log_weights);
+
+ if (contrast_weight > 0) {
+ Contrast(
+ image,
+ [&log_weights, contrast_weight](const size_t x, const size_t y,
+ const float weight) {
+ log_weights.Row(y)[x] = contrast_weight * std::log(weight);
+ },
+ pool);
+ }
+
+ if (saturation_weight > 0) {
+ Saturation(image, [&log_weights, saturation_weight](
+ const size_t x, const size_t y, const float weight) {
+ log_weights.Row(y)[x] += saturation_weight * std::log(weight);
+ });
+ }
+
+ if (midtoneness_weight > 0) {
+ MidToneness(image, midtoneness_sigma,
+ [&log_weights, midtoneness_weight](
+ const size_t x, const size_t y, const float weight) {
+ log_weights.Row(y)[x] +=
+ midtoneness_weight * std::log(weight);
+ });
+ }
+
+ ImageF weights = std::move(log_weights);
+
+ for (size_t y = 0; y < weights.ysize(); ++y) {
+ float* const JXL_RESTRICT row = weights.Row(y);
+ for (size_t x = 0; x < weights.xsize(); ++x) {
+ row[x] = std::exp(row[x]);
+ }
+ }
+
+ return weights;
+}
+
+std::vector<ImageF> ComputeWeights(const std::vector<Image3F>& images,
+ const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma,
+ ThreadPool* const pool) {
+ std::vector<ImageF> weights;
+ weights.reserve(images.size());
+ for (const Image3F& image : images) {
+ if (image.xsize() != images.front().xsize() ||
+ image.ysize() != images.front().ysize()) {
+ return {};
+ }
+ weights.push_back(ComputeWeights(image, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma,
+ pool));
+ }
+
+ std::vector<float*> rows(images.size());
+ for (size_t y = 0; y < images.front().ysize(); ++y) {
+ for (size_t i = 0; i < images.size(); ++i) {
+ rows[i] = weights[i].Row(y);
+ }
+ for (size_t x = 0; x < images.front().xsize(); ++x) {
+ float sum = 1e-9f;
+ for (size_t i = 0; i < images.size(); ++i) {
+ sum += rows[i][x];
+ }
+ const float ratio = 1.f / sum;
+ for (size_t i = 0; i < images.size(); ++i) {
+ rows[i][x] *= ratio;
+ }
+ }
+ }
+
+ return weights;
+}
+
+ImageF Downsample(const ImageF& image, ThreadPool* const pool) {
+ ImageF filtered(image.xsize(), image.ysize());
+ Separable5(image, Rect(image), kPyramidFilter, pool, &filtered);
+ ImageF result(DivCeil(image.xsize(), 2), DivCeil(image.ysize(), 2));
+ for (size_t y = 0; y < result.ysize(); ++y) {
+ const float* const JXL_RESTRICT filtered_row = filtered.ConstRow(2 * y);
+ float* const JXL_RESTRICT row = result.Row(y);
+ for (size_t x = 0; x < result.xsize(); ++x) {
+ row[x] = filtered_row[2 * x];
+ }
+ }
+ return result;
+}
+
+Image3F Downsample(const Image3F& image, ThreadPool* const pool) {
+ return Image3F(Downsample(image.Plane(0), pool),
+ Downsample(image.Plane(1), pool),
+ Downsample(image.Plane(2), pool));
+}
+
+Image3F PadImageMirror(const Image3F& in, const size_t xborder,
+ const size_t yborder) {
+ size_t xsize = in.xsize();
+ size_t ysize = in.ysize();
+ Image3F out(xsize + 2 * xborder, ysize + 2 * yborder);
+ if (xborder > xsize || yborder > ysize) {
+ for (size_t c = 0; c < 3; c++) {
+ for (int32_t y = 0; y < static_cast<int32_t>(out.ysize()); y++) {
+ float* row_out = out.PlaneRow(c, y);
+ const float* row_in = in.PlaneRow(
+ c, Mirror(y - static_cast<int32_t>(yborder), in.ysize()));
+ for (int32_t x = 0; x < static_cast<int32_t>(out.xsize()); x++) {
+ int32_t xin = Mirror(x - static_cast<int32_t>(xborder), in.xsize());
+ row_out[x] = row_in[xin];
+ }
+ }
+ }
+ return out;
+ }
+ CopyImageTo(Rect(in), in, Rect(xborder, yborder, xsize, ysize), &out);
+ for (size_t c = 0; c < 3; c++) {
+ // Horizontal pad.
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xborder; x++) {
+ out.PlaneRow(c, y + yborder)[x] =
+ in.ConstPlaneRow(c, y)[xborder - x - 1];
+ out.PlaneRow(c, y + yborder)[x + xsize + xborder] =
+ in.ConstPlaneRow(c, y)[xsize - 1 - x];
+ }
+ }
+ // Vertical pad.
+ for (size_t y = 0; y < yborder; y++) {
+ memcpy(out.PlaneRow(c, y), out.ConstPlaneRow(c, 2 * yborder - 1 - y),
+ out.xsize() * sizeof(float));
+ memcpy(out.PlaneRow(c, y + ysize + yborder),
+ out.ConstPlaneRow(c, ysize + yborder - 1 - y),
+ out.xsize() * sizeof(float));
+ }
+ }
+ return out;
+}
+
+Image3F Upsample(const Image3F& image, const bool odd_width,
+ const bool odd_height, ThreadPool* const pool) {
+ const Image3F padded = PadImageMirror(image, 1, 1);
+ Image3F upsampled(2 * padded.xsize(), 2 * padded.ysize());
+ ZeroFillImage(&upsampled);
+ for (int c = 0; c < 3; ++c) {
+ for (size_t y = 0; y < padded.ysize(); ++y) {
+ const float* const JXL_RESTRICT padded_row = padded.ConstPlaneRow(c, y);
+ float* const JXL_RESTRICT row = upsampled.PlaneRow(c, 2 * y);
+ for (size_t x = 0; x < padded.xsize(); ++x) {
+ row[2 * x] = 4 * padded_row[x];
+ }
+ }
+ }
+ Image3F filtered(upsampled.xsize(), upsampled.ysize());
+ for (int c = 0; c < 3; ++c) {
+ Separable5(upsampled.Plane(c), Rect(upsampled), kPyramidFilter, pool,
+ &filtered.Plane(c));
+ }
+ Image3F result(2 * image.xsize() - (odd_width ? 1 : 0),
+ 2 * image.ysize() - (odd_height ? 1 : 0));
+ CopyImageTo(Rect(2, 2, result.xsize(), result.ysize()), filtered,
+ Rect(result), &result);
+ return result;
+}
+
+std::vector<ImageF> GaussianPyramid(ImageF image, int num_levels,
+ ThreadPool* pool) {
+ std::vector<ImageF> pyramid(num_levels);
+ for (int i = 0; i < num_levels - 1; ++i) {
+ ImageF downsampled = Downsample(image, pool);
+ pyramid[i] = std::move(image);
+ image = std::move(downsampled);
+ }
+ pyramid[num_levels - 1] = std::move(image);
+ return pyramid;
+}
+
+std::vector<Image3F> LaplacianPyramid(Image3F image, int num_levels,
+ ThreadPool* pool) {
+ std::vector<Image3F> pyramid(num_levels);
+ for (int i = 0; i < num_levels - 1; ++i) {
+ Image3F downsampled = Downsample(image, pool);
+ const bool odd_width = image.xsize() % 2 != 0;
+ const bool odd_height = image.ysize() % 2 != 0;
+ Subtract(image, Upsample(downsampled, odd_width, odd_height, pool), &image);
+ pyramid[i] = std::move(image);
+ image = std::move(downsampled);
+ }
+ pyramid[num_levels - 1] = std::move(image);
+ return pyramid;
+}
+
+Image3F ReconstructFromLaplacianPyramid(std::vector<Image3F> pyramid,
+ ThreadPool* const pool) {
+ Image3F result = std::move(pyramid.back());
+ pyramid.pop_back();
+ for (auto it = pyramid.rbegin(); it != pyramid.rend(); ++it) {
+ const bool odd_width = it->xsize() % 2 != 0;
+ const bool odd_height = it->ysize() % 2 != 0;
+ result = Upsample(result, odd_width, odd_height, pool);
+ AddTo(Rect(result), *it, &result);
+ }
+ return result;
+}
+
+// Exposure fusion algorithm as described in:
+// https://mericam.github.io/exposure_fusion/
+//
+// That is, given n images of identical size: for each pixel coordinate, one
+// weight per input image is computed, indicating how much each input image will
+// contribute to the result. There are therefore n weight maps, the sum of which
+// is 1 at every pixel.
+//
+// Those weights are then applied at various scales rather than directly at full
+// resolution. To understand how, it helps to familiarize oneself with Laplacian
+// and Gaussian pyramids, as described in "The Laplacian Pyramid as a Compact
+// Image Code" by P. Burt and E. Adelson:
+// http://persci.mit.edu/pub_pdfs/pyramid83.pdf
+//
+// A Gaussian pyramid of k levels is a sequence of k images in which the first
+// image is the original image and each following level is a low-pass-filtered
+// version of the previous one. A Laplacian pyramid is obtained from a Gaussian
+// pyramid by:
+//
+// laplacian_pyramid[i] = gaussian_pyramid[i] − gaussian_pyramid[i + 1].
+// (The last item of the Laplacian pyramid is just the last one from the
+// Gaussian pyramid without subtraction.)
+//
+// From there, the original image can be reconstructed by adding all the images
+// from the Laplacian pyramid together. (If desired, the Gaussian pyramid can be
+// reconstructed as well by storing the cumulative sums starting from the end.)
+//
+// Having established that, the application of the weight images is done by
+// constructing a Laplacian pyramid for each input image, as well as a Gaussian
+// pyramid for each weight image, and then constructing a Laplacian pyramid such
+// that:
+//
+// pyramid[i] = sum(laplacian_pyramids[j][i] .* weight_gaussian_pyramids[j][i]
+// for j in 1..n)
+//
+// And then reconstructing an image from the pyramid thus obtained.
+Image3F ExposureFusion(std::vector<Image3F> images, int num_levels,
+ const float contrast_weight,
+ const float saturation_weight,
+ const float midtoneness_weight,
+ const float midtoneness_sigma, ThreadPool* const pool) {
+ std::vector<ImageF> weights =
+ ComputeWeights(images, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma, pool);
+
+ std::vector<Image3F> pyramid(num_levels);
+ for (size_t i = 0; i < images.size(); ++i) {
+ const std::vector<ImageF> weight_pyramid =
+ GaussianPyramid(std::move(weights[i]), num_levels, pool);
+ const std::vector<Image3F> image_pyramid =
+ LaplacianPyramid(std::move(images[i]), num_levels, pool);
+
+ for (int k = 0; k < num_levels; ++k) {
+ Image3F product(Product(weight_pyramid[k], image_pyramid[k].Plane(0)),
+ Product(weight_pyramid[k], image_pyramid[k].Plane(1)),
+ Product(weight_pyramid[k], image_pyramid[k].Plane(2)));
+ if (pyramid[k].xsize() == 0) {
+ pyramid[k] = std::move(product);
+ } else {
+ AddTo(Rect(product), product, &pyramid[k]);
+ }
+ }
+ }
+
+ return ReconstructFromLaplacianPyramid(std::move(pyramid), pool);
+}
+
+} // namespace
+} // namespace jxl
+
+int main(int argc, const char** argv) {
+ jpegxl::tools::ThreadPoolInternal pool;
+
+ jpegxl::tools::CommandLineParser parser;
+ float max_nits = 0;
+ parser.AddOptionValue('m', "max_nits", "nits",
+ "maximum luminance in the image", &max_nits,
+ &jpegxl::tools::ParseFloat, 0);
+ float preserve_saturation = .1f;
+ parser.AddOptionValue(
+ 's', "preserve_saturation", "0..1",
+ "to what extent to try and preserve saturation over luminance",
+ &preserve_saturation, &jpegxl::tools::ParseFloat, 0);
+ int64_t num_levels = -1;
+ parser.AddOptionValue('l', "num_levels", "1..",
+ "number of levels in the pyramid", &num_levels,
+ &jpegxl::tools::ParseInt64, 0);
+ float contrast_weight = 0.f;
+ parser.AddOptionValue('c', "contrast_weight", "0..",
+ "importance of contrast when computing weights",
+ &contrast_weight, &jpegxl::tools::ParseFloat, 0);
+ float saturation_weight = .2f;
+ parser.AddOptionValue('a', "saturation_weight", "0..",
+ "importance of saturation when computing weights",
+ &saturation_weight, &jpegxl::tools::ParseFloat, 0);
+ float midtoneness_weight = 1.f;
+ parser.AddOptionValue('t', "midtoneness_weight", "0..",
+ "importance of \"midtoneness\" when computing weights",
+ &midtoneness_weight, &jpegxl::tools::ParseFloat, 0);
+ float midtoneness_sigma = .2f;
+ parser.AddOptionValue('g', "midtoneness_sigma", "0..",
+ "spread of the function that computes midtoneness",
+ &midtoneness_sigma, &jpegxl::tools::ParseFloat, 0);
+ const char* input_filename = nullptr;
+ auto input_filename_option = parser.AddPositionalOption(
+ "input", true, "input image", &input_filename, 0);
+ const char* output_filename = nullptr;
+ auto output_filename_option = parser.AddPositionalOption(
+ "output", true, "output image", &output_filename, 0);
+
+ if (!parser.Parse(argc, argv)) {
+ fprintf(stderr, "See -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ if (parser.HelpFlagPassed()) {
+ parser.PrintHelp();
+ return EXIT_SUCCESS;
+ }
+
+ if (!parser.GetOption(input_filename_option)->matched()) {
+ fprintf(stderr, "Missing input filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+ if (!parser.GetOption(output_filename_option)->matched()) {
+ fprintf(stderr, "Missing output filename.\nSee -h for help.\n");
+ return EXIT_FAILURE;
+ }
+
+ jxl::CodecInOut image;
+ jxl::extras::ColorHints color_hints;
+ color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
+
+ if (max_nits > 0) {
+ image.metadata.m.SetIntensityTarget(max_nits);
+ } else {
+ max_nits = image.metadata.m.IntensityTarget();
+ }
+
+ std::vector<jxl::Image3F> input_images;
+
+ if (max_nits <= 4 * jxl::kDefaultIntensityTarget) {
+ jxl::CodecInOut sRGB_image;
+ jxl::Image3F color(image.xsize(), image.ysize());
+ CopyImageTo(*image.Main().color(), &color);
+ sRGB_image.SetFromImage(std::move(color), image.Main().c_current());
+ JXL_CHECK(sRGB_image.Main().TransformTo(jxl::ColorEncoding::SRGB(),
+ *JxlGetDefaultCms(), &pool));
+ input_images.push_back(std::move(*sRGB_image.Main().color()));
+ }
+
+ for (int i = 0; i < 4; ++i) {
+ const float target = std::ldexp(jxl::kDefaultIntensityTarget, 2 - i);
+ if (target >= max_nits) continue;
+ jxl::CodecInOut tone_mapped_image;
+ jxl::Image3F color(image.xsize(), image.ysize());
+ CopyImageTo(*image.Main().color(), &color);
+ tone_mapped_image.SetFromImage(std::move(color), image.Main().c_current());
+ tone_mapped_image.metadata.m.SetIntensityTarget(
+ image.metadata.m.IntensityTarget());
+ JXL_CHECK(jxl::ToneMapTo({0, target}, &tone_mapped_image, &pool));
+ JXL_CHECK(jxl::GamutMap(&tone_mapped_image, preserve_saturation, &pool));
+ JXL_CHECK(tone_mapped_image.Main().TransformTo(jxl::ColorEncoding::SRGB(),
+ *JxlGetDefaultCms(), &pool));
+ input_images.push_back(std::move(*tone_mapped_image.Main().color()));
+ }
+
+ if (num_levels < 1) {
+ num_levels = jxl::FloorLog2Nonzero(std::min(image.xsize(), image.ysize()));
+ }
+
+ jxl::Image3F fused = jxl::ExposureFusion(
+ std::move(input_images), num_levels, contrast_weight, saturation_weight,
+ midtoneness_weight, midtoneness_sigma, &pool);
+
+ jxl::CodecInOut output;
+ output.SetFromImage(std::move(fused), jxl::ColorEncoding::SRGB());
+
+ JXL_CHECK(jxl::Encode(output, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
+}
diff --git a/tools/hdr/pq_to_hlg.cc b/tools/hdr/pq_to_hlg.cc
index 3b2125b..ea47a6b 100644
--- a/tools/hdr/pq_to_hlg.cc
+++ b/tools/hdr/pq_to_hlg.cc
@@ -9,13 +9,13 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -56,10 +56,14 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
if (max_nits > 0) {
image.metadata.m.SetIntensityTarget(max_nits);
}
+ const jxl::Primaries original_primaries =
+ image.Main().c_current().GetPrimariesType();
JXL_CHECK(jxl::ToneMapTo({0, 1000}, &image, &pool));
JXL_CHECK(jxl::HlgInverseOOTF(&image.Main(), 1.2f, &pool));
JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
@@ -70,11 +74,12 @@ int main(int argc, const char** argv) {
jxl::ColorEncoding hlg;
hlg.SetColorSpace(jxl::ColorSpace::kRGB);
- hlg.primaries = jxl::Primaries::k2100;
- hlg.white_point = jxl::WhitePoint::kD65;
- hlg.tf.SetTransferFunction(jxl::TransferFunction::kHLG);
+ JXL_CHECK(hlg.SetPrimariesType(original_primaries));
+ JXL_CHECK(hlg.SetWhitePointType(jxl::WhitePoint::kD65));
+ hlg.Tf().SetTransferFunction(jxl::TransferFunction::kHLG);
JXL_CHECK(hlg.CreateICC());
- JXL_CHECK(image.TransformTo(hlg, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, hlg, &pool));
image.metadata.m.color_encoding = hlg;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/render_hlg.cc b/tools/hdr/render_hlg.cc
index c8a2395..cca43b1 100644
--- a/tools/hdr/render_hlg.cc
+++ b/tools/hdr/render_hlg.cc
@@ -9,13 +9,13 @@
#include "lib/extras/codec.h"
#include "lib/extras/hlg.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float target_nits = 0;
@@ -71,7 +71,9 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_HLG");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
// Ensures that conversions to linear by JxlCms will not apply the OOTF as we
// apply it ourselves to control the subsequent gamut mapping.
image.metadata.m.SetIntensityTarget(301);
@@ -82,13 +84,12 @@ int main(int argc, const char** argv) {
image.metadata.m.SetIntensityTarget(target_nits);
jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
- if (pq) {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
- } else {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
- }
+ jxl::cms::TransferFunction tf =
+ pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB;
+ c_out.Tf().SetTransferFunction(tf);
JXL_CHECK(c_out.CreateICC());
- JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool));
image.metadata.m.color_encoding = c_out;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}
diff --git a/tools/hdr/texture_to_cube.cc b/tools/hdr/texture_to_cube.cc
index a5e5af7..0d9f731 100644
--- a/tools/hdr/texture_to_cube.cc
+++ b/tools/hdr/texture_to_cube.cc
@@ -7,12 +7,13 @@
#include <stdlib.h>
#include "lib/extras/codec.h"
-#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/image_bundle.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
const char* input_filename = nullptr;
@@ -42,8 +43,10 @@ int main(int argc, const char** argv) {
}
jxl::CodecInOut image;
- JXL_CHECK(jxl::SetFromFile(input_filename, jxl::extras::ColorHints(), &image,
- &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), jxl::extras::ColorHints(),
+ &image, &pool));
JXL_CHECK(image.xsize() == image.ysize() * image.ysize());
const unsigned N = image.ysize();
diff --git a/tools/hdr/tone_map.cc b/tools/hdr/tone_map.cc
index 1ef3823..67fea48 100644
--- a/tools/hdr/tone_map.cc
+++ b/tools/hdr/tone_map.cc
@@ -8,13 +8,14 @@
#include "lib/extras/codec.h"
#include "lib/extras/tone_mapping.h"
-#include "lib/jxl/base/thread_pool_internal.h"
-#include "lib/jxl/enc_color_management.h"
#include "tools/args.h"
#include "tools/cmdline.h"
+#include "tools/file_io.h"
+#include "tools/hdr/image_utils.h"
+#include "tools/thread_pool_internal.h"
int main(int argc, const char** argv) {
- jxl::ThreadPoolInternal pool;
+ jpegxl::tools::ThreadPoolInternal pool;
jpegxl::tools::CommandLineParser parser;
float max_nits = 0;
@@ -69,7 +70,9 @@ int main(int argc, const char** argv) {
jxl::CodecInOut image;
jxl::extras::ColorHints color_hints;
color_hints.Add("color_space", "RGB_D65_202_Rel_PeQ");
- JXL_CHECK(jxl::SetFromFile(input_filename, color_hints, &image, &pool));
+ std::vector<uint8_t> encoded;
+ JXL_CHECK(jpegxl::tools::ReadFile(input_filename, &encoded));
+ JXL_CHECK(jxl::SetFromBytes(jxl::Bytes(encoded), color_hints, &image, &pool));
if (max_nits > 0) {
image.metadata.m.SetIntensityTarget(max_nits);
}
@@ -77,13 +80,18 @@ int main(int argc, const char** argv) {
JXL_CHECK(jxl::GamutMap(&image, preserve_saturation, &pool));
jxl::ColorEncoding c_out = image.metadata.m.color_encoding;
- if (pq) {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::kPQ);
- } else {
- c_out.tf.SetTransferFunction(jxl::TransferFunction::k709);
+ jxl::cms::TransferFunction tf =
+ pq ? jxl::TransferFunction::kPQ : jxl::TransferFunction::kSRGB;
+
+ if (jxl::extras::CodecFromPath(output_filename) == jxl::extras::Codec::kEXR) {
+ tf = jxl::TransferFunction::kLinear;
+ image.metadata.m.SetFloat16Samples();
}
+ c_out.Tf().SetTransferFunction(tf);
+
JXL_CHECK(c_out.CreateICC());
- JXL_CHECK(image.TransformTo(c_out, jxl::GetJxlCms(), &pool));
+ JXL_CHECK(jpegxl::tools::TransformCodecInOutTo(image, c_out, &pool));
image.metadata.m.color_encoding = c_out;
- JXL_CHECK(jxl::EncodeToFile(image, output_filename, &pool));
+ JXL_CHECK(jxl::Encode(image, output_filename, &encoded, &pool));
+ JXL_CHECK(jpegxl::tools::WriteFile(output_filename, encoded));
}