// 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 "lib/extras/enc/jpegli.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lib/extras/enc/encode.h" #include "lib/extras/packed_image.h" #include "lib/jpegli/common.h" #include "lib/jpegli/encode.h" #include "lib/jpegli/types.h" #include "lib/jxl/base/byte_order.h" #include "lib/jxl/base/common.h" #include "lib/jxl/base/data_parallel.h" #include "lib/jxl/base/status.h" #include "lib/jxl/color_encoding_internal.h" #include "lib/jxl/enc_xyb.h" #include "lib/jxl/image.h" #include "lib/jxl/simd_util.h" namespace jxl { namespace extras { namespace { void MyErrorExit(j_common_ptr cinfo) { jmp_buf* env = static_cast(cinfo->client_data); (*cinfo->err->output_message)(cinfo); jpegli_destroy_compress(reinterpret_cast(cinfo)); longjmp(*env, 1); } Status VerifyInput(const PackedPixelFile& ppf) { const JxlBasicInfo& info = ppf.info; JXL_RETURN_IF_ERROR(Encoder::VerifyBasicInfo(info)); if (ppf.frames.size() != 1) { return JXL_FAILURE("JPEG input must have exactly one frame."); } if (info.num_color_channels != 1 && info.num_color_channels != 3) { return JXL_FAILURE("Invalid number of color channels %d", info.num_color_channels); } const PackedImage& image = ppf.frames[0].color; JXL_RETURN_IF_ERROR(Encoder::VerifyImageSize(image, info)); if (image.format.data_type == JXL_TYPE_FLOAT16) { return JXL_FAILURE("FLOAT16 input is not supported."); } JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type, info.bits_per_sample, info.exponent_bits_per_sample)); if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) || (image.format.data_type == JXL_TYPE_UINT16 && info.bits_per_sample != 16)) { return JXL_FAILURE("Only full bit depth unsigned types are supported."); } return true; } Status GetColorEncoding(const PackedPixelFile& ppf, ColorEncoding* color_encoding) { if (ppf.primary_color_representation == PackedPixelFile::kIccIsPrimary) { IccBytes icc = ppf.icc; JXL_RETURN_IF_ERROR( color_encoding->SetICC(std::move(icc), JxlGetDefaultCms())); } else { JXL_RETURN_IF_ERROR(color_encoding->FromExternal(ppf.color_encoding)); } if (color_encoding->ICC().empty()) { return JXL_FAILURE("Invalid color encoding."); } return true; } bool HasICCProfile(const std::vector& app_data) { size_t pos = 0; while (pos < app_data.size()) { if (pos + 16 > app_data.size()) return false; uint8_t marker = app_data[pos + 1]; size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2; if (marker == 0xe2 && memcmp(&app_data[pos + 4], "ICC_PROFILE", 12) == 0) { return true; } pos += marker_len; } return false; } Status WriteAppData(j_compress_ptr cinfo, const std::vector& app_data) { size_t pos = 0; while (pos < app_data.size()) { if (pos + 4 > app_data.size()) { return JXL_FAILURE("Incomplete APP header."); } uint8_t marker = app_data[pos + 1]; size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2; if (app_data[pos] != 0xff || marker < 0xe0 || marker > 0xef) { return JXL_FAILURE("Invalid APP marker %02x %02x", app_data[pos], marker); } if (marker_len <= 4) { return JXL_FAILURE("Invalid APP marker length."); } if (pos + marker_len > app_data.size()) { return JXL_FAILURE("Incomplete APP data"); } jpegli_write_marker(cinfo, marker, &app_data[pos + 4], marker_len - 4); pos += marker_len; } return true; } constexpr int kICCMarker = 0xe2; constexpr unsigned char kICCSignature[12] = { 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; constexpr uint8_t kUnknownTf = 2; constexpr unsigned char kCICPTagSignature[4] = {0x63, 0x69, 0x63, 0x70}; constexpr size_t kCICPTagSize = 12; bool FindCICPTag(const uint8_t* icc_data, size_t len, bool is_first_chunk, size_t* cicp_offset, size_t* cicp_length, uint8_t* cicp_tag, size_t* cicp_pos) { if (is_first_chunk) { // Look up the offset of the CICP tag from the first chunk of ICC data. if (len < 132) { return false; } uint32_t tag_count = LoadBE32(&icc_data[128]); if (len < 132 + 12 * tag_count) { return false; } for (uint32_t i = 0; i < tag_count; ++i) { if (memcmp(&icc_data[132 + 12 * i], kCICPTagSignature, 4) == 0) { *cicp_offset = LoadBE32(&icc_data[136 + 12 * i]); *cicp_length = LoadBE32(&icc_data[140 + 12 * i]); } } if (*cicp_length < kCICPTagSize) { return false; } } if (*cicp_offset < len) { size_t n_bytes = std::min(len - *cicp_offset, kCICPTagSize - *cicp_pos); memcpy(&cicp_tag[*cicp_pos], &icc_data[*cicp_offset], n_bytes); *cicp_pos += n_bytes; *cicp_offset = 0; } else { *cicp_offset -= len; } return true; } uint8_t LookupCICPTransferFunctionFromAppData(const uint8_t* app_data, size_t len) { size_t last_index = 0; size_t cicp_offset = 0; size_t cicp_length = 0; uint8_t cicp_tag[kCICPTagSize] = {}; size_t cicp_pos = 0; size_t pos = 0; while (pos < len) { const uint8_t* marker = &app_data[pos]; if (pos + 4 > len) { return kUnknownTf; } size_t marker_size = (marker[2] << 8) + marker[3] + 2; if (pos + marker_size > len) { return kUnknownTf; } if (marker_size < 18 || marker[0] != 0xff || marker[1] != kICCMarker || memcmp(&marker[4], kICCSignature, 12) != 0) { pos += marker_size; continue; } uint8_t index = marker[16]; uint8_t total = marker[17]; const uint8_t* payload = marker + 18; const size_t payload_size = marker_size - 18; if (index != last_index + 1 || index > total) { return kUnknownTf; } if (!FindCICPTag(payload, payload_size, last_index == 0, &cicp_offset, &cicp_length, &cicp_tag[0], &cicp_pos)) { return kUnknownTf; } if (cicp_pos == kCICPTagSize) { break; } ++last_index; } if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) { return cicp_tag[9]; } return kUnknownTf; } uint8_t LookupCICPTransferFunctionFromICCProfile(const uint8_t* icc_data, size_t len) { size_t cicp_offset = 0; size_t cicp_length = 0; uint8_t cicp_tag[kCICPTagSize] = {}; size_t cicp_pos = 0; if (!FindCICPTag(icc_data, len, true, &cicp_offset, &cicp_length, &cicp_tag[0], &cicp_pos)) { return kUnknownTf; } if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) { return cicp_tag[9]; } return kUnknownTf; } JpegliDataType ConvertDataType(JxlDataType type) { switch (type) { case JXL_TYPE_UINT8: return JPEGLI_TYPE_UINT8; case JXL_TYPE_UINT16: return JPEGLI_TYPE_UINT16; case JXL_TYPE_FLOAT: return JPEGLI_TYPE_FLOAT; default: return JPEGLI_TYPE_UINT8; } } JpegliEndianness ConvertEndianness(JxlEndianness endianness) { switch (endianness) { case JXL_NATIVE_ENDIAN: return JPEGLI_NATIVE_ENDIAN; case JXL_LITTLE_ENDIAN: return JPEGLI_LITTLE_ENDIAN; case JXL_BIG_ENDIAN: return JPEGLI_BIG_ENDIAN; default: return JPEGLI_NATIVE_ENDIAN; } } void ToFloatRow(const uint8_t* row_in, JxlPixelFormat format, size_t xsize, size_t c_out, float* row_out) { bool is_little_endian = (format.endianness == JXL_LITTLE_ENDIAN || (format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian())); static constexpr double kMul8 = 1.0 / 255.0; static constexpr double kMul16 = 1.0 / 65535.0; const size_t c_in = format.num_channels; if (format.data_type == JXL_TYPE_UINT8) { for (size_t x = 0; x < xsize; ++x) { for (size_t c = 0; c < c_out; ++c) { const size_t ix = c_in * x + c; row_out[c_out * x + c] = row_in[ix] * kMul8; } } } else if (format.data_type == JXL_TYPE_UINT16 && is_little_endian) { for (size_t x = 0; x < xsize; ++x) { for (size_t c = 0; c < c_out; ++c) { const size_t ix = c_in * x + c; row_out[c_out * x + c] = LoadLE16(&row_in[2 * ix]) * kMul16; } } } else if (format.data_type == JXL_TYPE_UINT16 && !is_little_endian) { for (size_t x = 0; x < xsize; ++x) { for (size_t c = 0; c < c_out; ++c) { const size_t ix = c_in * x + c; row_out[c_out * x + c] = LoadBE16(&row_in[2 * ix]) * kMul16; } } } else if (format.data_type == JXL_TYPE_FLOAT && is_little_endian) { for (size_t x = 0; x < xsize; ++x) { for (size_t c = 0; c < c_out; ++c) { const size_t ix = c_in * x + c; row_out[c_out * x + c] = LoadLEFloat(&row_in[4 * ix]); } } } else if (format.data_type == JXL_TYPE_FLOAT && !is_little_endian) { for (size_t x = 0; x < xsize; ++x) { for (size_t c = 0; c < c_out; ++c) { const size_t ix = c_in * x + c; row_out[c_out * x + c] = LoadBEFloat(&row_in[4 * ix]); } } } } Status EncodeJpegToTargetSize(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, size_t target_size, ThreadPool* pool, std::vector* output) { output->clear(); size_t best_error = std::numeric_limits::max(); float distance0 = -1.0f; float distance1 = -1.0f; float distance = 1.0f; for (int step = 0; step < 15; ++step) { JpegSettings settings = jpeg_settings; settings.libjpeg_quality = 0; settings.distance = distance; settings.target_size = 0; std::vector compressed; JXL_RETURN_IF_ERROR(EncodeJpeg(ppf, settings, pool, &compressed)); size_t size = compressed.size(); // prefer being under the target size to being over it size_t error = size < target_size ? target_size - size : static_cast(1.2f * (size - target_size)); if (error < best_error) { best_error = error; std::swap(*output, compressed); } float rel_error = size * 1.0f / target_size; if (std::abs(rel_error - 1.0f) < 0.002f) { break; } if (size < target_size) { distance1 = distance; } else { distance0 = distance; } if (distance1 == -1) { distance *= std::pow(rel_error, 1.5) * 1.05; } else if (distance0 == -1) { distance *= std::pow(rel_error, 1.5) * 0.95; } else { distance = 0.5 * (distance0 + distance1); } } return true; } } // namespace Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, ThreadPool* pool, std::vector* compressed) { if (jpeg_settings.libjpeg_quality > 0) { auto encoder = Encoder::FromExtension(".jpg"); encoder->SetOption("q", std::to_string(jpeg_settings.libjpeg_quality)); if (!jpeg_settings.libjpeg_chroma_subsampling.empty()) { encoder->SetOption("chroma_subsampling", jpeg_settings.libjpeg_chroma_subsampling); } EncodedImage encoded; JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool)); size_t target_size = encoded.bitstreams[0].size(); return EncodeJpegToTargetSize(ppf, jpeg_settings, target_size, pool, compressed); } if (jpeg_settings.target_size > 0) { return EncodeJpegToTargetSize(ppf, jpeg_settings, jpeg_settings.target_size, pool, compressed); } JXL_RETURN_IF_ERROR(VerifyInput(ppf)); ColorEncoding color_encoding; JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding)); ColorSpaceTransform c_transform(*JxlGetDefaultCms()); ColorEncoding xyb_encoding; if (jpeg_settings.xyb) { if (HasICCProfile(jpeg_settings.app_data)) { return JXL_FAILURE("APP data ICC profile is not supported in XYB mode."); } const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(false); JXL_RETURN_IF_ERROR( c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1)); xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB); xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual); JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC()); } const ColorEncoding& output_encoding = jpeg_settings.xyb ? xyb_encoding : color_encoding; // We need to declare all the non-trivial destructor local variables // before the call to setjmp(). std::vector pixels; unsigned char* output_buffer = nullptr; unsigned long output_size = 0; std::vector row_bytes; size_t rowlen = RoundUpTo(ppf.info.xsize, MaxVectorSize()); hwy::AlignedFreeUniquePtr xyb_tmp = hwy::AllocateAligned(6 * rowlen); hwy::AlignedFreeUniquePtr premul_absorb = hwy::AllocateAligned(MaxVectorSize() * 12); ComputePremulAbsorb(255.0f, premul_absorb.get()); jpeg_compress_struct cinfo; const auto try_catch_block = [&]() -> bool { jpeg_error_mgr jerr; jmp_buf env; cinfo.err = jpegli_std_error(&jerr); jerr.error_exit = &MyErrorExit; if (setjmp(env)) { return false; } cinfo.client_data = static_cast(&env); jpegli_create_compress(&cinfo); jpegli_mem_dest(&cinfo, &output_buffer, &output_size); const JxlBasicInfo& info = ppf.info; cinfo.image_width = info.xsize; cinfo.image_height = info.ysize; cinfo.input_components = info.num_color_channels; cinfo.in_color_space = cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB; if (jpeg_settings.xyb) { jpegli_set_xyb_mode(&cinfo); cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; } else if (jpeg_settings.use_std_quant_tables) { jpegli_use_standard_quant_tables(&cinfo); } uint8_t cicp_tf = kUnknownTf; if (!jpeg_settings.app_data.empty()) { cicp_tf = LookupCICPTransferFunctionFromAppData( jpeg_settings.app_data.data(), jpeg_settings.app_data.size()); } else if (!output_encoding.IsSRGB()) { cicp_tf = LookupCICPTransferFunctionFromICCProfile( output_encoding.ICC().data(), output_encoding.ICC().size()); } jpegli_set_cicp_transfer_function(&cinfo, cicp_tf); jpegli_set_defaults(&cinfo); if (!jpeg_settings.chroma_subsampling.empty()) { if (jpeg_settings.chroma_subsampling == "444") { cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 1; } else if (jpeg_settings.chroma_subsampling == "440") { cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 2; } else if (jpeg_settings.chroma_subsampling == "422") { cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 1; } else if (jpeg_settings.chroma_subsampling == "420") { cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 2; } else { return false; } for (int i = 1; i < cinfo.num_components; ++i) { cinfo.comp_info[i].h_samp_factor = 1; cinfo.comp_info[i].v_samp_factor = 1; } } jpegli_enable_adaptive_quantization( &cinfo, TO_JXL_BOOL(jpeg_settings.use_adaptive_quantization)); if (jpeg_settings.psnr_target > 0.0) { jpegli_set_psnr(&cinfo, jpeg_settings.psnr_target, jpeg_settings.search_tolerance, jpeg_settings.min_distance, jpeg_settings.max_distance); } else if (jpeg_settings.quality > 0.0) { float distance = jpegli_quality_to_distance(jpeg_settings.quality); jpegli_set_distance(&cinfo, distance, TRUE); } else { jpegli_set_distance(&cinfo, jpeg_settings.distance, TRUE); } jpegli_set_progressive_level(&cinfo, jpeg_settings.progressive_level); cinfo.optimize_coding = TO_JXL_BOOL(jpeg_settings.optimize_coding); if (!jpeg_settings.app_data.empty()) { // Make sure jpegli_start_compress() does not write any APP markers. cinfo.write_JFIF_header = JXL_FALSE; cinfo.write_Adobe_marker = JXL_FALSE; } const PackedImage& image = ppf.frames[0].color; if (jpeg_settings.xyb) { jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN); } else { jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type), ConvertEndianness(image.format.endianness)); } jpegli_start_compress(&cinfo, TRUE); if (!jpeg_settings.app_data.empty()) { JXL_RETURN_IF_ERROR(WriteAppData(&cinfo, jpeg_settings.app_data)); } if ((jpeg_settings.app_data.empty() && !output_encoding.IsSRGB()) || jpeg_settings.xyb) { jpegli_write_icc_profile(&cinfo, output_encoding.ICC().data(), output_encoding.ICC().size()); } const uint8_t* pixels = reinterpret_cast(image.pixels()); if (jpeg_settings.xyb) { float* src_buf = c_transform.BufSrc(0); float* dst_buf = c_transform.BufDst(0); for (size_t y = 0; y < image.ysize; ++y) { // convert to float ToFloatRow(&pixels[y * image.stride], image.format, image.xsize, info.num_color_channels, src_buf); // convert to linear srgb if (!c_transform.Run(0, src_buf, dst_buf, image.xsize)) { return false; } // deinterleave channels float* row0 = &xyb_tmp[0]; float* row1 = &xyb_tmp[rowlen]; float* row2 = &xyb_tmp[2 * rowlen]; for (size_t x = 0; x < image.xsize; ++x) { row0[x] = dst_buf[3 * x + 0]; row1[x] = dst_buf[3 * x + 1]; row2[x] = dst_buf[3 * x + 2]; } // convert to xyb LinearRGBRowToXYB(row0, row1, row2, premul_absorb.get(), image.xsize); // scale xyb ScaleXYBRow(row0, row1, row2, image.xsize); // interleave channels float* row_out = &xyb_tmp[3 * rowlen]; for (size_t x = 0; x < image.xsize; ++x) { row_out[3 * x + 0] = row0[x]; row_out[3 * x + 1] = row1[x]; row_out[3 * x + 2] = row2[x]; } // feed to jpegli as native endian floats JSAMPROW row[] = {reinterpret_cast(row_out)}; jpegli_write_scanlines(&cinfo, row, 1); } } else { row_bytes.resize(image.stride); if (cinfo.num_components == static_cast(image.format.num_channels)) { for (size_t y = 0; y < info.ysize; ++y) { memcpy(row_bytes.data(), pixels + y * image.stride, image.stride); JSAMPROW row[] = {row_bytes.data()}; jpegli_write_scanlines(&cinfo, row, 1); } } else { for (size_t y = 0; y < info.ysize; ++y) { int bytes_per_channel = PackedImage::BitsPerChannel(image.format.data_type) / 8; int bytes_per_pixel = cinfo.num_components * bytes_per_channel; for (size_t x = 0; x < info.xsize; ++x) { memcpy(&row_bytes[x * bytes_per_pixel], &pixels[y * image.stride + x * image.pixel_stride()], bytes_per_pixel); } JSAMPROW row[] = {row_bytes.data()}; jpegli_write_scanlines(&cinfo, row, 1); } } } jpegli_finish_compress(&cinfo); compressed->resize(output_size); std::copy_n(output_buffer, output_size, compressed->data()); return true; }; bool success = try_catch_block(); jpegli_destroy_compress(&cinfo); if (output_buffer) free(output_buffer); return success; } } // namespace extras } // namespace jxl