// 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/encode.h" #include "enc_color_management.h" #include "gtest/gtest.h" #include "jxl/decode.h" #include "jxl/decode_cxx.h" #include "jxl/encode_cxx.h" #include "lib/extras/codec.h" #include "lib/jxl/dec_file.h" #include "lib/jxl/enc_butteraugli_pnorm.h" #include "lib/jxl/encode_internal.h" #include "lib/jxl/jpeg/dec_jpeg_data.h" #include "lib/jxl/jpeg/dec_jpeg_data_writer.h" #include "lib/jxl/test_utils.h" #include "lib/jxl/testdata.h" TEST(EncodeTest, AddFrameAfterCloseInputTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderCloseInput(enc.get()); size_t xsize = 64; size_t ysize = 64; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); jxl::CodecInOut input_io = jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); } TEST(EncodeTest, AddJPEGAfterCloseTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderCloseInput(enc.get()); const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddJPEGFrame(frame_settings, orig.data(), orig.size())); } TEST(EncodeTest, AddFrameBeforeColorEncodingTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); size_t xsize = 64; size_t ysize = 64; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); jxl::CodecInOut input_io = jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = true; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); } TEST(EncodeTest, AddFrameBeforeBasicInfoTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); size_t xsize = 64; size_t ysize = 64; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); jxl::CodecInOut input_io = jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); } TEST(EncodeTest, DefaultAllocTest) { JxlEncoder* enc = JxlEncoderCreate(nullptr); EXPECT_NE(nullptr, enc); JxlEncoderDestroy(enc); } TEST(EncodeTest, CustomAllocTest) { struct CalledCounters { int allocs = 0; int frees = 0; } counters; JxlMemoryManager mm; mm.opaque = &counters; mm.alloc = [](void* opaque, size_t size) { reinterpret_cast(opaque)->allocs++; return malloc(size); }; mm.free = [](void* opaque, void* address) { reinterpret_cast(opaque)->frees++; free(address); }; { JxlEncoderPtr enc = JxlEncoderMake(&mm); EXPECT_NE(nullptr, enc.get()); EXPECT_LE(1, counters.allocs); EXPECT_EQ(0, counters.frees); } EXPECT_LE(1, counters.frees); } TEST(EncodeTest, DefaultParallelRunnerTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetParallelRunner(enc.get(), nullptr, nullptr)); } void VerifyFrameEncoding(size_t xsize, size_t ysize, JxlEncoder* enc, const JxlEncoderFrameSettings* frame_settings) { JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); jxl::CodecInOut input_io = jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; if (frame_settings->values.lossless) { basic_info.uses_original_profile = true; } else { basic_info.uses_original_profile = false; } // 16-bit alpha means this requires level 10 EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc, 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc, &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, true); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetColorEncoding(enc, &color_encoding)); JxlColorEncodingSetToSRGB(&color_encoding, false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc, &color_encoding)); pixel_format.num_channels = 1; EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); pixel_format.num_channels = 4; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseInput(enc); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); jxl::DecompressParams dparams; jxl::CodecInOut decoded_io; EXPECT_TRUE(jxl::DecodeFile( dparams, jxl::Span(compressed.data(), compressed.size()), &decoded_io, /*pool=*/nullptr)); EXPECT_LE( ComputeDistance2(input_io.Main(), decoded_io.Main(), jxl::GetJxlCms()), #if JXL_HIGH_PRECISION 1.8); #else 4.8); #endif } void VerifyFrameEncoding(JxlEncoder* enc, const JxlEncoderFrameSettings* frame_settings) { VerifyFrameEncoding(63, 129, enc, frame_settings); } TEST(EncodeTest, FrameEncodingTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); VerifyFrameEncoding(enc.get(), JxlEncoderFrameSettingsCreate(enc.get(), nullptr)); } TEST(EncodeTest, EncoderResetTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); VerifyFrameEncoding(50, 200, enc.get(), JxlEncoderFrameSettingsCreate(enc.get(), nullptr)); // Encoder should become reusable for a new image from scratch after using // reset. JxlEncoderReset(enc.get()); VerifyFrameEncoding(157, 77, enc.get(), JxlEncoderFrameSettingsCreate(enc.get(), nullptr)); } TEST(EncodeTest, CmsTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); bool cms_called = false; JxlCmsInterface cms = jxl::GetJxlCms(); struct InitData { void* original_init_data; jpegxl_cms_init_func original_init; bool* cms_called; }; InitData init_data = {/*original_init_data=*/cms.init_data, /*original_init=*/cms.init, /*cms_called=*/&cms_called}; cms.init_data = &init_data; cms.init = +[](void* raw_init_data, size_t num_threads, size_t pixels_per_thread, const JxlColorProfile* input_profile, const JxlColorProfile* output_profile, float intensity_target) { const InitData* init_data = static_cast(raw_init_data); *init_data->cms_called = true; return init_data->original_init(init_data->original_init_data, num_threads, pixels_per_thread, input_profile, output_profile, intensity_target); }; JxlEncoderSetCms(enc.get(), cms); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr); JxlEncoderSetFrameLossless(frame_settings, false); ASSERT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 8)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_TRUE(cms_called); } TEST(EncodeTest, frame_settingsTest) { { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 5)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(jxl::SpeedTier::kHare, enc->last_used_cparams.speed_tier); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); // Lower than currently supported values EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 0)); // Higher than currently supported values EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 10)); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(true, enc->last_used_cparams.IsLossless()); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetFrameDistance(frame_settings, 0.5)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(0.5, enc->last_used_cparams.butteraugli_distance); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); // Disallowed negative distance EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetFrameDistance(frame_settings, -1)); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_DECODING_SPEED, 2)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(2u, enc->last_used_cparams.decoding_speed_tier); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_GROUP_ORDER, 100)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_GROUP_ORDER, 1)); EXPECT_EQ( JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X, 5)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(true, enc->last_used_cparams.centerfirst); EXPECT_EQ(5, enc->last_used_cparams.center_x); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_RESPONSIVE, 0)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, 1)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC, -1)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, 2)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(false, enc->last_used_cparams.responsive); EXPECT_EQ(true, enc->last_used_cparams.progressive_mode); EXPECT_EQ(2, enc->last_used_cparams.progressive_dc); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_PHOTON_NOISE, 1777)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(1777.0f, enc->last_used_cparams.photon_noise_iso); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT, 55)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT, 25)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_PALETTE_COLORS, 70000)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_LOSSY_PALETTE, 1)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(55.0f, enc->last_used_cparams.channel_colors_pre_transform_percent); EXPECT_EQ(25.0f, enc->last_used_cparams.channel_colors_percent); EXPECT_EQ(70000, enc->last_used_cparams.palette_colors); EXPECT_EQ(true, enc->last_used_cparams.lossy_palette); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ( JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE, 30)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, 2)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, 14)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT, 77)); EXPECT_EQ( JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS, 7)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(30, enc->last_used_cparams.colorspace); EXPECT_EQ(2, enc->last_used_cparams.modular_group_size_shift); EXPECT_EQ(jxl::Predictor::Best, enc->last_used_cparams.options.predictor); EXPECT_EQ(0.77f, enc->last_used_cparams.options.nb_repeats); EXPECT_EQ(7, enc->last_used_cparams.options.max_properties); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL, 0)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(false, enc->last_used_cparams.force_cfl_jpeg_recompression); } { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderFrameSettingsSetOption( frame_settings, JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL, 1)); VerifyFrameEncoding(enc.get(), frame_settings); EXPECT_EQ(true, enc->last_used_cparams.force_cfl_jpeg_recompression); } } namespace { // Returns a copy of buf from offset to offset+size, or a new zeroed vector if // the result would have been out of bounds taking integer overflow into // account. const std::vector SliceSpan(const jxl::Span& buf, size_t offset, size_t size) { if (offset + size >= buf.size()) { return std::vector(size, 0); } if (offset + size < offset) { return std::vector(size, 0); } return std::vector(buf.data() + offset, buf.data() + offset + size); } struct Box { // The type of the box. // If "uuid", use extended_type instead char type[4] = {0, 0, 0, 0}; // The extended_type is only used when type == "uuid". // Extended types are not used in JXL. However, the box format itself // supports this so they are handled correctly. char extended_type[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Box data. jxl::Span data = jxl::Span(nullptr, 0); // If the size is not given, the datasize extends to the end of the file. // If this field is false, the size field is not encoded when the box is // serialized. bool data_size_given = true; // If successful, returns true and sets `in` to be the rest data (if any). // If `in` contains a box with a size larger than `in.size()`, will not // modify `in`, and will return true but the data `Span` will // remain set to nullptr. // If unsuccessful, returns error and doesn't modify `in`. jxl::Status Decode(jxl::Span* in) { // Total box_size including this header itself. uint64_t box_size = LoadBE32(SliceSpan(*in, 0, 4).data()); size_t pos = 4; memcpy(type, SliceSpan(*in, pos, 4).data(), 4); pos += 4; if (box_size == 1) { // If the size is 1, it indicates extended size read from 64-bit integer. box_size = LoadBE64(SliceSpan(*in, pos, 8).data()); pos += 8; } if (!memcmp("uuid", type, 4)) { memcpy(extended_type, SliceSpan(*in, pos, 16).data(), 16); pos += 16; } // This is the end of the box header, the box data begins here. Handle // the data size now. const size_t header_size = pos; if (box_size != 0) { if (box_size < header_size) { return JXL_FAILURE("Invalid box size"); } if (box_size > in->size()) { // The box is fine, but the input is too short. return true; } data_size_given = true; data = jxl::Span(in->data() + header_size, box_size - header_size); } else { data_size_given = false; data = jxl::Span(in->data() + header_size, in->size() - header_size); } *in = jxl::Span(in->data() + header_size + data.size(), in->size() - header_size - data.size()); return true; } }; struct Container { std::vector boxes; // If successful, returns true and sets `in` to be the rest data (if any). // If unsuccessful, returns error and doesn't modify `in`. jxl::Status Decode(jxl::Span* in) { boxes.clear(); Box signature_box; JXL_RETURN_IF_ERROR(signature_box.Decode(in)); if (memcmp("JXL ", signature_box.type, 4) != 0) { return JXL_FAILURE("Invalid magic signature"); } if (signature_box.data.size() != 4) return JXL_FAILURE("Invalid magic signature"); if (signature_box.data[0] != 0xd || signature_box.data[1] != 0xa || signature_box.data[2] != 0x87 || signature_box.data[3] != 0xa) { return JXL_FAILURE("Invalid magic signature"); } Box ftyp_box; JXL_RETURN_IF_ERROR(ftyp_box.Decode(in)); if (memcmp("ftyp", ftyp_box.type, 4) != 0) { return JXL_FAILURE("Invalid ftyp"); } if (ftyp_box.data.size() != 12) return JXL_FAILURE("Invalid ftyp"); const char* expected = "jxl \0\0\0\0jxl "; if (memcmp(expected, ftyp_box.data.data(), 12) != 0) return JXL_FAILURE("Invalid ftyp"); while (!in->empty()) { Box box = {}; JXL_RETURN_IF_ERROR(box.Decode(in)); if (box.data.data() == nullptr) { // The decoding encountered a box, but not enough data yet. return true; } boxes.emplace_back(box); } return true; } }; } // namespace TEST(EncodeTest, SingleFrameBoundedJXLCTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseContainer(enc.get(), true)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); size_t xsize = 71; size_t ysize = 23; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseInput(enc.get()); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); Container container = {}; jxl::Span encoded_span = jxl::Span(compressed.data(), compressed.size()); EXPECT_TRUE(container.Decode(&encoded_span)); EXPECT_EQ(0u, encoded_span.size()); bool found_jxlc = false; bool found_jxlp = false; // The encoder is allowed to either emit a jxlc or one or more jxlp. for (size_t i = 0; i < container.boxes.size(); ++i) { if (memcmp("jxlc", container.boxes[i].type, 4) == 0) { EXPECT_EQ(false, found_jxlc); // Max 1 jxlc EXPECT_EQ(false, found_jxlp); // Can't mix jxlc and jxlp found_jxlc = true; } if (memcmp("jxlp", container.boxes[i].type, 4) == 0) { EXPECT_EQ(false, found_jxlc); // Can't mix jxlc and jxlp found_jxlp = true; } // The encoder shouldn't create an unbounded box in this case, with the // single frame it knows the full size in time, so can help make decoding // more efficient by giving the full box size of the final box. EXPECT_EQ(true, container.boxes[i].data_size_given); } EXPECT_EQ(true, found_jxlc || found_jxlp); } TEST(EncodeTest, CodestreamLevelTest) { size_t xsize = 64; size_t ysize = 64; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); jxl::CodecInOut input_io = jxl::test::SomeTestImageToCodecInOut(pixels, 4, xsize, ysize); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/pixel_format.num_channels < 3); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseInput(enc.get()); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); Container container = {}; jxl::Span encoded_span = jxl::Span(compressed.data(), compressed.size()); EXPECT_TRUE(container.Decode(&encoded_span)); EXPECT_EQ(0u, encoded_span.size()); EXPECT_EQ(0, memcmp("jxll", container.boxes[0].type, 4)); } TEST(EncodeTest, CodestreamLevelVerificationTest) { JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0}; JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = 64; basic_info.ysize = 64; basic_info.uses_original_profile = false; JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); EXPECT_EQ(5, JxlEncoderGetRequiredCodestreamLevel(enc.get())); // Set an image dimension that is too large for level 5, but fits in level 10 basic_info.xsize = 1ull << 30ull; EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); EXPECT_EQ(10, JxlEncoderGetRequiredCodestreamLevel(enc.get())); // Set an image dimension that is too large even for level 10 basic_info.xsize = 1ull << 31ull; EXPECT_EQ(JXL_ENC_ERROR, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); } TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGReconstructionTest)) { const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); JxlEncoderPtr enc = JxlEncoderMake(nullptr); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderStoreJPEGMetadata(enc.get(), JXL_TRUE)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddJPEGFrame(frame_settings, orig.data(), orig.size())); JxlEncoderCloseInput(enc.get()); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); Container container = {}; jxl::Span encoded_span = jxl::Span(compressed.data(), compressed.size()); EXPECT_TRUE(container.Decode(&encoded_span)); EXPECT_EQ(0u, encoded_span.size()); bool found_jbrd = false; bool found_jxlc = false; bool found_jxlp = false; size_t jbrd_index = 0; std::vector codestream_bytes; // The encoder is allowed to either emit a jxlc or one or more jxlp. for (size_t i = 0; i < container.boxes.size(); ++i) { if (memcmp("jbrd", container.boxes[i].type, 4) == 0) { EXPECT_EQ(false, found_jxlc); // Max 1 jbrd found_jbrd = true; jbrd_index = i; } if (memcmp("jxlc", container.boxes[i].type, 4) == 0) { EXPECT_EQ(false, found_jxlc); // Max 1 jxlc EXPECT_EQ(false, found_jxlp); // Can't mix jxlc and jxlp found_jxlc = true; codestream_bytes.insert( codestream_bytes.end(), container.boxes[i].data.data(), container.boxes[i].data.data() + container.boxes[i].data.size()); } if (memcmp("jxlp", container.boxes[i].type, 4) == 0) { EXPECT_EQ(false, found_jxlc); // Can't mix jxlc and jxlp found_jxlp = true; // Append all data except the first 4 box content bytes which are the // jxpl box counter. codestream_bytes.insert( codestream_bytes.end(), container.boxes[i].data.data() + 4, container.boxes[i].data.data() + container.boxes[i].data.size()); } } EXPECT_EQ(true, found_jbrd); EXPECT_EQ(true, found_jxlc || found_jxlp); jxl::CodecInOut decoded_io; decoded_io.Main().jpeg_data = jxl::make_unique(); EXPECT_TRUE(jxl::jpeg::DecodeJPEGData(container.boxes[jbrd_index].data, decoded_io.Main().jpeg_data.get())); jxl::DecompressParams dparams; dparams.keep_dct = true; jxl::Span codestream_span = jxl::Span( codestream_bytes.data(), codestream_bytes.size()); EXPECT_TRUE(jxl::DecodeFile(dparams, codestream_span, &decoded_io, nullptr)); std::vector decoded_jpeg_bytes; auto write = [&decoded_jpeg_bytes](const uint8_t* buf, size_t len) { decoded_jpeg_bytes.insert(decoded_jpeg_bytes.end(), buf, buf + len); return len; }; EXPECT_TRUE(jxl::jpeg::WriteJpeg(*decoded_io.Main().jpeg_data, write)); EXPECT_EQ(decoded_jpeg_bytes.size(), orig.size()); EXPECT_EQ(0, memcmp(decoded_jpeg_bytes.data(), orig.data(), orig.size())); } static void ProcessEncoder(JxlEncoder* enc, std::vector& compressed, uint8_t*& next_out, size_t& avail_out) { JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc, &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } size_t offset = next_out - compressed.data(); compressed.resize(next_out - compressed.data()); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; EXPECT_EQ(JXL_ENC_SUCCESS, process_result); } TEST(EncodeTest, BasicInfoTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); size_t xsize = 1; size_t ysize = 1; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; basic_info.have_animation = true; basic_info.intensity_target = 123.4; basic_info.min_nits = 5.0; basic_info.linear_below = 12.7; basic_info.orientation = JXL_ORIENT_ROTATE_90_CW; basic_info.animation.tps_numerator = 55; basic_info.animation.tps_denominator = 77; basic_info.animation.num_loops = 10; basic_info.animation.have_timecodes = JXL_TRUE; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseFrames(enc.get()); ProcessEncoder(enc.get(), compressed, next_out, avail_out); // Decode to verify the boxes, we don't decode to pixels, only the boxes. JxlDecoderPtr dec = JxlDecoderMake(nullptr); EXPECT_NE(nullptr, dec.get()); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO)); // Allow testing the orientation field, without this setting it will be // overridden to identity. JxlDecoderSetKeepOrientation(dec.get(), JXL_TRUE); JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); JxlDecoderCloseInput(dec.get()); for (;;) { JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); if (status == JXL_DEC_ERROR) { FAIL(); } else if (status == JXL_DEC_SUCCESS) { break; } else if (status == JXL_DEC_BASIC_INFO) { JxlBasicInfo basic_info2; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec.get(), &basic_info2)); EXPECT_EQ(basic_info.xsize, basic_info2.xsize); EXPECT_EQ(basic_info.ysize, basic_info2.ysize); EXPECT_EQ(basic_info.bits_per_sample, basic_info2.bits_per_sample); EXPECT_EQ(basic_info.exponent_bits_per_sample, basic_info2.exponent_bits_per_sample); EXPECT_NEAR(basic_info.intensity_target, basic_info2.intensity_target, 0.5); EXPECT_NEAR(basic_info.min_nits, basic_info2.min_nits, 0.5); EXPECT_NEAR(basic_info.linear_below, basic_info2.linear_below, 0.5); EXPECT_EQ(basic_info.relative_to_max_display, basic_info2.relative_to_max_display); EXPECT_EQ(basic_info.uses_original_profile, basic_info2.uses_original_profile); EXPECT_EQ(basic_info.orientation, basic_info2.orientation); EXPECT_EQ(basic_info.num_color_channels, basic_info2.num_color_channels); // TODO(lode): also test num_extra_channels, but currently there may be a // mismatch between 0 and 1 if there is alpha, until encoder support for // extra channels is fully implemented. EXPECT_EQ(basic_info.alpha_bits, basic_info2.alpha_bits); EXPECT_EQ(basic_info.alpha_exponent_bits, basic_info2.alpha_exponent_bits); EXPECT_EQ(basic_info.alpha_premultiplied, basic_info2.alpha_premultiplied); EXPECT_EQ(basic_info.have_preview, basic_info2.have_preview); if (basic_info.have_preview) { EXPECT_EQ(basic_info.preview.xsize, basic_info2.preview.xsize); EXPECT_EQ(basic_info.preview.ysize, basic_info2.preview.ysize); } EXPECT_EQ(basic_info.have_animation, basic_info2.have_animation); if (basic_info.have_animation) { EXPECT_EQ(basic_info.animation.tps_numerator, basic_info2.animation.tps_numerator); EXPECT_EQ(basic_info.animation.tps_denominator, basic_info2.animation.tps_denominator); EXPECT_EQ(basic_info.animation.num_loops, basic_info2.animation.num_loops); EXPECT_EQ(basic_info.animation.have_timecodes, basic_info2.animation.have_timecodes); } } else { FAIL(); // unexpected status } } } TEST(EncodeTest, AnimationHeaderTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); size_t xsize = 1; size_t ysize = 1; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.have_animation = true; basic_info.animation.tps_numerator = 1000; basic_info.animation.tps_denominator = 1; basic_info.animation.have_timecodes = JXL_TRUE; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); std::string frame_name = "test frame"; JxlFrameHeader header; JxlEncoderInitFrameHeader(&header); header.duration = 50; header.timecode = 800; header.layer_info.blend_info.blendmode = JXL_BLEND_BLEND; header.layer_info.blend_info.source = 2; header.layer_info.blend_info.clamp = 1; JxlBlendInfo extra_channel_blend_info; JxlEncoderInitBlendInfo(&extra_channel_blend_info); extra_channel_blend_info.blendmode = JXL_BLEND_MULADD; JxlEncoderSetFrameHeader(frame_settings, &header); JxlEncoderSetExtraChannelBlendInfo(frame_settings, 0, &extra_channel_blend_info); JxlEncoderSetFrameName(frame_settings, frame_name.c_str()); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseFrames(enc.get()); ProcessEncoder(enc.get(), compressed, next_out, avail_out); // Decode to verify the boxes, we don't decode to pixels, only the boxes. JxlDecoderPtr dec = JxlDecoderMake(nullptr); EXPECT_NE(nullptr, dec.get()); // To test the blend_info fields, coalescing must be set to false in the // decoder. EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetCoalescing(dec.get(), JXL_FALSE)); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_FRAME)); JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); JxlDecoderCloseInput(dec.get()); bool seen_frame = false; for (;;) { JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); if (status == JXL_DEC_ERROR) { FAIL(); } else if (status == JXL_DEC_SUCCESS) { break; } else if (status == JXL_DEC_FRAME) { seen_frame = true; JxlFrameHeader header2; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec.get(), &header2)); EXPECT_EQ(header.duration, header2.duration); EXPECT_EQ(header.timecode, header2.timecode); EXPECT_EQ(header.layer_info.blend_info.blendmode, header2.layer_info.blend_info.blendmode); EXPECT_EQ(header.layer_info.blend_info.clamp, header2.layer_info.blend_info.clamp); EXPECT_EQ(header.layer_info.blend_info.source, header2.layer_info.blend_info.source); EXPECT_EQ(frame_name.size(), header2.name_length); JxlBlendInfo extra_channel_blend_info2; JxlDecoderGetExtraChannelBlendInfo(dec.get(), 0, &extra_channel_blend_info2); EXPECT_EQ(extra_channel_blend_info.blendmode, extra_channel_blend_info2.blendmode); if (header2.name_length > 0) { std::string frame_name2(header2.name_length + 1, '\0'); EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameName(dec.get(), &frame_name2.front(), frame_name2.size())); frame_name2.resize(header2.name_length); EXPECT_EQ(frame_name, frame_name2); } } else { FAIL(); // unexpected status } } EXPECT_EQ(true, seen_frame); } TEST(EncodeTest, CroppedFrameTest) { JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); size_t xsize = 300; size_t ysize = 300; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); std::vector pixels2(pixels.size()); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); // Encoding a 300x300 frame in an image that is only 100x100 basic_info.xsize = 100; basic_info.ysize = 100; basic_info.uses_original_profile = JXL_TRUE; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); JxlFrameHeader header; JxlEncoderInitFrameHeader(&header); header.layer_info.have_crop = JXL_TRUE; header.layer_info.xsize = xsize; header.layer_info.ysize = ysize; header.layer_info.crop_x0 = -50; header.layer_info.crop_y0 = -250; JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE); JxlEncoderSetFrameHeader(frame_settings, &header); JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 1); std::vector compressed = std::vector(100); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); JxlEncoderCloseFrames(enc.get()); ProcessEncoder(enc.get(), compressed, next_out, avail_out); JxlDecoderPtr dec = JxlDecoderMake(nullptr); EXPECT_NE(nullptr, dec.get()); // Non-coalesced decoding so we can get the full uncropped frame EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetCoalescing(dec.get(), JXL_FALSE)); EXPECT_EQ( JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE)); JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); JxlDecoderCloseInput(dec.get()); bool seen_frame = false; bool checked_frame = false; for (;;) { JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); if (status == JXL_DEC_ERROR) { FAIL(); } else if (status == JXL_DEC_SUCCESS) { break; } else if (status == JXL_DEC_FRAME) { seen_frame = true; JxlFrameHeader header2; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetFrameHeader(dec.get(), &header2)); EXPECT_EQ(header.layer_info.xsize, header2.layer_info.xsize); EXPECT_EQ(header.layer_info.ysize, header2.layer_info.ysize); EXPECT_EQ(header.layer_info.crop_x0, header2.layer_info.crop_x0); EXPECT_EQ(header.layer_info.crop_y0, header2.layer_info.crop_y0); } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetImageOutBuffer(dec.get(), &pixel_format, pixels2.data(), pixels2.size())); } else if (status == JXL_DEC_FULL_IMAGE) { EXPECT_EQ(0, memcmp(pixels.data(), pixels2.data(), pixels.size())); checked_frame = true; } else { FAIL(); // unexpected status } } EXPECT_EQ(true, checked_frame); EXPECT_EQ(true, seen_frame); } TEST(EncodeTest, BoxTest) { // Test with uncompressed boxes and with brob boxes for (int compress_box = 0; compress_box <= 1; ++compress_box) { // Tests adding two metadata boxes with the encoder: an exif box before the // image frame, and an xml box after the image frame. Then verifies the // decoder can decode them, they are in the expected place, and have the // correct content after decoding. JxlEncoderPtr enc = JxlEncoderMake(nullptr); EXPECT_NE(nullptr, enc.get()); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderUseBoxes(enc.get())); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); size_t xsize = 50; size_t ysize = 17; JxlPixelFormat pixel_format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0}; std::vector pixels = jxl::test::GetSomeTestImage(xsize, ysize, 4, 0); JxlBasicInfo basic_info; jxl::test::JxlBasicInfoSetFromPixelFormat(&basic_info, &pixel_format); basic_info.xsize = xsize; basic_info.ysize = ysize; basic_info.uses_original_profile = false; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetCodestreamLevel(enc.get(), 10)); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); // Add an early metadata box. Also add a valid 4-byte TIFF offset header // before the fake exif data of these box contents. constexpr const char* exif_test_string = "\0\0\0\0exif test data"; const uint8_t* exif_data = reinterpret_cast(exif_test_string); // Skip the 4 zeroes for strlen const size_t exif_size = 4 + strlen(exif_test_string + 4); JxlEncoderAddBox(enc.get(), "Exif", exif_data, exif_size, compress_box); // Write to output ProcessEncoder(enc.get(), compressed, next_out, avail_out); // Add image frame EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddImageFrame(frame_settings, &pixel_format, pixels.data(), pixels.size())); // Indicate this is the last frame JxlEncoderCloseFrames(enc.get()); // Write to output ProcessEncoder(enc.get(), compressed, next_out, avail_out); // Add a late metadata box constexpr const char* xml_test_string = ""; const uint8_t* xml_data = reinterpret_cast(xml_test_string); size_t xml_size = strlen(xml_test_string); JxlEncoderAddBox(enc.get(), "XML ", xml_data, xml_size, compress_box); // Indicate this is the last box JxlEncoderCloseBoxes(enc.get()); // Write to output ProcessEncoder(enc.get(), compressed, next_out, avail_out); // Decode to verify the boxes, we don't decode to pixels, only the boxes. JxlDecoderPtr dec = JxlDecoderMake(nullptr); EXPECT_NE(nullptr, dec.get()); if (compress_box) { EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetDecompressBoxes(dec.get(), JXL_TRUE)); } EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents( dec.get(), JXL_DEC_FRAME | JXL_DEC_BOX)); JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); JxlDecoderCloseInput(dec.get()); std::vector dec_exif_box(exif_size); std::vector dec_xml_box(xml_size); for (bool post_frame = false;;) { JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); if (status == JXL_DEC_ERROR) { FAIL(); } else if (status == JXL_DEC_SUCCESS) { EXPECT_EQ(0, JxlDecoderReleaseBoxBuffer(dec.get())); break; } else if (status == JXL_DEC_FRAME) { post_frame = true; } else if (status == JXL_DEC_BOX) { // Since we gave the exif/xml box output buffer of the exact known // correct size, 0 bytes should be released. Same when no buffer was // set. EXPECT_EQ(0, JxlDecoderReleaseBoxBuffer(dec.get())); JxlBoxType type; EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBoxType(dec.get(), type, true)); if (!memcmp(type, "Exif", 4)) { // This box should have been encoded before the image frame EXPECT_EQ(false, post_frame); JxlDecoderSetBoxBuffer(dec.get(), dec_exif_box.data(), dec_exif_box.size()); } else if (!memcmp(type, "XML ", 4)) { // This box should have been encoded after the image frame EXPECT_EQ(true, post_frame); JxlDecoderSetBoxBuffer(dec.get(), dec_xml_box.data(), dec_xml_box.size()); } } else { FAIL(); // unexpected status } } EXPECT_EQ(0, memcmp(exif_data, dec_exif_box.data(), exif_size)); EXPECT_EQ(0, memcmp(xml_data, dec_xml_box.data(), xml_size)); } } #if JPEGXL_ENABLE_JPEG // Loading .jpg files requires libjpeg support. TEST(EncodeTest, JXL_TRANSCODE_JPEG_TEST(JPEGFrameTest)) { for (int skip_basic_info = 0; skip_basic_info < 2; skip_basic_info++) { for (int skip_color_encoding = 0; skip_color_encoding < 2; skip_color_encoding++) { // cannot set color encoding if basic info is not set if (skip_basic_info && !skip_color_encoding) continue; const std::string jpeg_path = "jxl/flower/flower_cropped.jpg"; const jxl::PaddedBytes orig = jxl::ReadTestData(jpeg_path); jxl::CodecInOut orig_io; ASSERT_TRUE(SetFromBytes(jxl::Span(orig), &orig_io, /*pool=*/nullptr)); JxlEncoderPtr enc = JxlEncoderMake(nullptr); JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), NULL); JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 1); if (!skip_basic_info) { JxlBasicInfo basic_info; JxlEncoderInitBasicInfo(&basic_info); basic_info.xsize = orig_io.xsize(); basic_info.ysize = orig_io.ysize(); basic_info.uses_original_profile = true; EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetBasicInfo(enc.get(), &basic_info)); } if (!skip_color_encoding) { JxlColorEncoding color_encoding; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/false); EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderSetColorEncoding(enc.get(), &color_encoding)); } EXPECT_EQ(JXL_ENC_SUCCESS, JxlEncoderAddJPEGFrame( frame_settings, orig.data(), orig.size())); JxlEncoderCloseInput(enc.get()); std::vector compressed = std::vector(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size() - (next_out - compressed.data()); JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { size_t offset = next_out - compressed.data(); compressed.resize(compressed.size() * 2); next_out = compressed.data() + offset; avail_out = compressed.size() - offset; } } compressed.resize(next_out - compressed.data()); EXPECT_EQ(JXL_ENC_SUCCESS, process_result); jxl::DecompressParams dparams; jxl::CodecInOut decoded_io; EXPECT_TRUE(jxl::DecodeFile( dparams, jxl::Span(compressed.data(), compressed.size()), &decoded_io, /*pool=*/nullptr)); EXPECT_LE( ComputeDistance2(orig_io.Main(), decoded_io.Main(), jxl::GetJxlCms()), 3.5); } } } #endif // JPEGXL_ENABLE_JPEG