// 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/jpg.h" #if JPEGXL_ENABLE_JPEG #include "lib/jxl/base/include_jpeglib.h" // NOLINT #endif #include #include #include #include #include #include #include #include #include #include "lib/extras/exif.h" #include "lib/jxl/base/common.h" #include "lib/jxl/base/status.h" #include "lib/jxl/sanitizers.h" #if JPEGXL_ENABLE_SJPEG #include "sjpeg.h" #include "sjpegi.h" #endif namespace jxl { namespace extras { #if JPEGXL_ENABLE_JPEG namespace { constexpr unsigned char kICCSignature[12] = { 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; constexpr int kICCMarker = JPEG_APP0 + 2; constexpr size_t kMaxBytesInMarker = 65533; constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00}; constexpr int kExifMarker = JPEG_APP0 + 1; enum class JpegEncoder { kLibJpeg, kSJpeg, }; // Popular jpeg scan scripts // The fields of the individual scans are: // comps_in_scan, component_index[], Ss, Se, Ah, Al constexpr auto kScanScript1 = to_array({ {1, {0}, 0, 0, 0, 0}, // {1, {1}, 0, 0, 0, 0}, // {1, {2}, 0, 0, 0, 0}, // {1, {0}, 1, 8, 0, 0}, // {1, {0}, 9, 63, 0, 0}, // {1, {1}, 1, 63, 0, 0}, // {1, {2}, 1, 63, 0, 0} // }); constexpr size_t kNumScans1 = kScanScript1.size(); constexpr auto kScanScript2 = to_array({ {1, {0}, 0, 0, 0, 0}, // {1, {1}, 0, 0, 0, 0}, // {1, {2}, 0, 0, 0, 0}, // {1, {0}, 1, 2, 0, 1}, // {1, {0}, 3, 63, 0, 1}, // {1, {0}, 1, 63, 1, 0}, // {1, {1}, 1, 63, 0, 0}, // {1, {2}, 1, 63, 0, 0} // }); constexpr size_t kNumScans2 = kScanScript2.size(); constexpr auto kScanScript3 = to_array({ {1, {0}, 0, 0, 0, 0}, // {1, {1}, 0, 0, 0, 0}, // {1, {2}, 0, 0, 0, 0}, // {1, {0}, 1, 63, 0, 2}, // {1, {0}, 1, 63, 2, 1}, // {1, {0}, 1, 63, 1, 0}, // {1, {1}, 1, 63, 0, 0}, // {1, {2}, 1, 63, 0, 0} // }); constexpr size_t kNumScans3 = kScanScript3.size(); constexpr auto kScanScript4 = to_array({ {3, {0, 1, 2}, 0, 0, 0, 1}, // {1, {0}, 1, 5, 0, 2}, // {1, {2}, 1, 63, 0, 1}, // {1, {1}, 1, 63, 0, 1}, // {1, {0}, 6, 63, 0, 2}, // {1, {0}, 1, 63, 2, 1}, // {3, {0, 1, 2}, 0, 0, 1, 0}, // {1, {2}, 1, 63, 1, 0}, // {1, {1}, 1, 63, 1, 0}, // {1, {0}, 1, 63, 1, 0} // }); constexpr size_t kNumScans4 = kScanScript4.size(); constexpr auto kScanScript5 = to_array({ {3, {0, 1, 2}, 0, 0, 0, 1}, // {1, {0}, 1, 5, 0, 2}, // {1, {1}, 1, 5, 0, 2}, // {1, {2}, 1, 5, 0, 2}, // {1, {1}, 6, 63, 0, 2}, // {1, {2}, 6, 63, 0, 2}, // {1, {0}, 6, 63, 0, 2}, // {1, {0}, 1, 63, 2, 1}, // {1, {1}, 1, 63, 2, 1}, // {1, {2}, 1, 63, 2, 1}, // {3, {0, 1, 2}, 0, 0, 1, 0}, // {1, {0}, 1, 63, 1, 0}, // {1, {1}, 1, 63, 1, 0}, // {1, {2}, 1, 63, 1, 0} // }); constexpr size_t kNumScans5 = kScanScript5.size(); // default progressive mode of jpegli constexpr auto kScanScript6 = to_array({ {3, {0, 1, 2}, 0, 0, 0, 0}, // {1, {0}, 1, 2, 0, 0}, // {1, {1}, 1, 2, 0, 0}, // {1, {2}, 1, 2, 0, 0}, // {1, {0}, 3, 63, 0, 2}, // {1, {1}, 3, 63, 0, 2}, // {1, {2}, 3, 63, 0, 2}, // {1, {0}, 3, 63, 2, 1}, // {1, {1}, 3, 63, 2, 1}, // {1, {2}, 3, 63, 2, 1}, // {1, {0}, 3, 63, 1, 0}, // {1, {1}, 3, 63, 1, 0}, // {1, {2}, 3, 63, 1, 0}, // }); constexpr size_t kNumScans6 = kScanScript6.size(); // Adapt RGB scan info to grayscale jpegs. void FilterScanComponents(const jpeg_compress_struct* cinfo, jpeg_scan_info* si) { const int all_comps_in_scan = si->comps_in_scan; si->comps_in_scan = 0; for (int j = 0; j < all_comps_in_scan; ++j) { const int component = si->component_index[j]; if (component < cinfo->input_components) { si->component_index[si->comps_in_scan++] = component; } } } Status SetJpegProgression(int progressive_id, std::vector* scan_infos, jpeg_compress_struct* cinfo) { if (progressive_id < 0) { return true; } if (progressive_id == 0) { jpeg_simple_progression(cinfo); return true; } const jpeg_scan_info* kScanScripts[] = { kScanScript1.data(), kScanScript2.data(), kScanScript3.data(), kScanScript4.data(), kScanScript5.data(), kScanScript6.data()}; constexpr auto kNumScans = to_array( {kNumScans1, kNumScans2, kNumScans3, kNumScans4, kNumScans5, kNumScans6}); if (progressive_id > static_cast(kNumScans.size())) { return JXL_FAILURE("Unknown jpeg scan script id %d", progressive_id); } const jpeg_scan_info* scan_script = kScanScripts[progressive_id - 1]; const size_t num_scans = kNumScans[progressive_id - 1]; // filter scan script for number of components for (size_t i = 0; i < num_scans; ++i) { jpeg_scan_info scan_info = scan_script[i]; FilterScanComponents(cinfo, &scan_info); if (scan_info.comps_in_scan > 0) { scan_infos->emplace_back(scan_info); } } cinfo->scan_info = scan_infos->data(); cinfo->num_scans = scan_infos->size(); return true; } void WriteICCProfile(jpeg_compress_struct* const cinfo, const std::vector& icc) { constexpr size_t kMaxIccBytesInMarker = kMaxBytesInMarker - sizeof kICCSignature - 2; const int num_markers = static_cast(DivCeil(icc.size(), kMaxIccBytesInMarker)); size_t begin = 0; for (int current_marker = 0; current_marker < num_markers; ++current_marker) { const size_t length = std::min(kMaxIccBytesInMarker, icc.size() - begin); jpeg_write_m_header( cinfo, kICCMarker, static_cast(length + sizeof kICCSignature + 2)); for (const unsigned char c : kICCSignature) { jpeg_write_m_byte(cinfo, c); } jpeg_write_m_byte(cinfo, current_marker + 1); jpeg_write_m_byte(cinfo, num_markers); for (size_t i = 0; i < length; ++i) { jpeg_write_m_byte(cinfo, icc[begin]); ++begin; } } } void WriteExif(jpeg_compress_struct* const cinfo, const std::vector& exif) { jpeg_write_m_header( cinfo, kExifMarker, static_cast(exif.size() + sizeof kExifSignature)); for (const unsigned char c : kExifSignature) { jpeg_write_m_byte(cinfo, c); } for (uint8_t c : exif) { jpeg_write_m_byte(cinfo, c); } } Status SetChromaSubsampling(const std::string& subsampling, jpeg_compress_struct* const cinfo) { const std::pair, std::array>> options[] = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; for (const auto& option : options) { if (subsampling == option.first) { for (size_t i = 0; i < 3; i++) { cinfo->comp_info[i].h_samp_factor = option.second.first[i]; cinfo->comp_info[i].v_samp_factor = option.second.second[i]; } return true; } } return false; } struct JpegParams { // Common between sjpeg and libjpeg int quality = 100; std::string chroma_subsampling = "444"; // Libjpeg parameters int progressive_id = -1; bool optimize_coding = true; bool is_xyb = false; // Sjpeg parameters int libjpeg_quality = 0; std::string libjpeg_chroma_subsampling = "444"; float psnr_target = 0; std::string custom_base_quant_fn; float search_q_start = 65.0f; float search_q_min = 1.0f; float search_q_max = 100.0f; int search_max_iters = 20; float search_tolerance = 0.1f; float search_q_precision = 0.01f; float search_first_iter_slope = 3.0f; bool enable_adaptive_quant = true; }; Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, const std::vector& icc, std::vector exif, const JpegParams& params, std::vector* bytes) { if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) { return JXL_FAILURE("Only 8 bit JSAMPLE is supported."); } jpeg_compress_struct cinfo = {}; jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); unsigned char* buffer = nullptr; unsigned long size = 0; // NOLINT jpeg_mem_dest(&cinfo, &buffer, &size); cinfo.image_width = image.xsize; cinfo.image_height = image.ysize; cinfo.input_components = info.num_color_channels; cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; jpeg_set_defaults(&cinfo); cinfo.optimize_coding = static_cast(params.optimize_coding); if (cinfo.input_components == 3) { JXL_RETURN_IF_ERROR( SetChromaSubsampling(params.chroma_subsampling, &cinfo)); } if (params.is_xyb) { // Tell libjpeg not to convert XYB data to YCbCr. jpeg_set_colorspace(&cinfo, JCS_RGB); } jpeg_set_quality(&cinfo, params.quality, TRUE); std::vector scan_infos; JXL_RETURN_IF_ERROR( SetJpegProgression(params.progressive_id, &scan_infos, &cinfo)); jpeg_start_compress(&cinfo, TRUE); if (!icc.empty()) { WriteICCProfile(&cinfo, icc); } if (!exif.empty()) { ResetExifOrientation(exif); WriteExif(&cinfo, exif); } if (cinfo.input_components > 3 || cinfo.input_components < 0) return JXL_FAILURE("invalid numbers of components"); std::vector row_bytes(image.stride); const uint8_t* pixels = reinterpret_cast(image.pixels()); if (cinfo.num_components == static_cast(image.format.num_channels) && image.format.data_type == JXL_TYPE_UINT8) { for (size_t y = 0; y < info.ysize; ++y) { memcpy(row_bytes.data(), pixels + y * image.stride, image.stride); JSAMPROW row[] = {row_bytes.data()}; jpeg_write_scanlines(&cinfo, row, 1); } } else if (image.format.data_type == JXL_TYPE_UINT8) { for (size_t y = 0; y < info.ysize; ++y) { const uint8_t* image_row = pixels + y * image.stride; for (size_t x = 0; x < info.xsize; ++x) { const uint8_t* image_pixel = image_row + x * image.pixel_stride(); memcpy(&row_bytes[x * cinfo.num_components], image_pixel, cinfo.num_components); } JSAMPROW row[] = {row_bytes.data()}; jpeg_write_scanlines(&cinfo, row, 1); } } else { for (size_t y = 0; y < info.ysize; ++y) { const uint8_t* image_row = pixels + y * image.stride; for (size_t x = 0; x < info.xsize; ++x) { const uint8_t* image_pixel = image_row + x * image.pixel_stride(); for (int c = 0; c < cinfo.num_components; ++c) { uint32_t val16 = (image_pixel[2 * c] << 8) + image_pixel[2 * c + 1]; row_bytes[x * cinfo.num_components + c] = (val16 + 128) / 257; } } JSAMPROW row[] = {row_bytes.data()}; jpeg_write_scanlines(&cinfo, row, 1); } } jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); bytes->resize(size); // Compressed image data is initialized by libjpeg, which we are not // instrumenting with msan. msan::UnpoisonMemory(buffer, size); std::copy_n(buffer, size, bytes->data()); std::free(buffer); return true; } #if JPEGXL_ENABLE_SJPEG struct MySearchHook : public sjpeg::SearchHook { uint8_t base_tables[2][64]; float q_start; float q_precision; float first_iter_slope; void ReadBaseTables(const std::string& fn) { const uint8_t kJPEGAnnexKMatrices[2][64] = { {16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99}, {17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}}; memcpy(base_tables[0], kJPEGAnnexKMatrices[0], sizeof(base_tables[0])); memcpy(base_tables[1], kJPEGAnnexKMatrices[1], sizeof(base_tables[1])); if (!fn.empty()) { std::ifstream f(fn); std::string line; int idx = 0; while (idx < 128 && std::getline(f, line)) { if (line.empty() || line[0] == '#') continue; std::istringstream line_stream(line); std::string token; while (idx < 128 && std::getline(line_stream, token, ',')) { uint8_t val = std::stoi(token); base_tables[idx / 64][idx % 64] = val; idx++; } } } } bool Setup(const sjpeg::EncoderParam& param) override { sjpeg::SearchHook::Setup(param); q = q_start; return true; } void NextMatrix(int idx, uint8_t dst[64]) override { float factor = (q <= 0) ? 5000.0f : (q < 50.0f) ? 5000.0f / q : (q < 100.0f) ? 2 * (100.0f - q) : 0.0f; sjpeg::SetQuantMatrix(base_tables[idx], factor, dst); } bool Update(float result) override { value = result; if (std::fabs(value - target) < tolerance * target) { return true; } if (value > target) { qmax = q; } else { qmin = q; } if (qmin == qmax) { return true; } const float last_q = q; if (pass == 0) { q += first_iter_slope * (for_size ? 0.1 * std::log(target / value) : (target - value)); q = std::max(qmin, std::min(qmax, q)); } else { q = (qmin + qmax) / 2.; } return (pass > 0 && std::fabs(q - last_q) < q_precision); } ~MySearchHook() override = default; }; #endif Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, const std::vector& icc, std::vector exif, const JpegParams& params, std::vector* bytes) { #if !JPEGXL_ENABLE_SJPEG return JXL_FAILURE("JPEG XL was built without sjpeg support"); #else if (image.format.data_type != JXL_TYPE_UINT8) { return JXL_FAILURE("Unsupported pixel data type"); } if (info.alpha_bits > 0) { return JXL_FAILURE("alpha is not supported"); } sjpeg::EncoderParam param(params.quality); if (!icc.empty()) { param.iccp.assign(icc.begin(), icc.end()); } if (!exif.empty()) { ResetExifOrientation(exif); param.exif.assign(exif.begin(), exif.end()); } if (params.chroma_subsampling == "444") { param.yuv_mode = SJPEG_YUV_444; } else if (params.chroma_subsampling == "420") { param.yuv_mode = SJPEG_YUV_420; } else if (params.chroma_subsampling == "420sharp") { param.yuv_mode = SJPEG_YUV_SHARP; } else { return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); } param.adaptive_quantization = params.enable_adaptive_quant; std::unique_ptr hook; if (params.libjpeg_quality > 0) { JpegParams libjpeg_params; libjpeg_params.quality = params.libjpeg_quality; libjpeg_params.chroma_subsampling = params.libjpeg_chroma_subsampling; std::vector libjpeg_bytes; JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, exif, libjpeg_params, &libjpeg_bytes)); param.target_mode = sjpeg::EncoderParam::TARGET_SIZE; param.target_value = libjpeg_bytes.size(); } if (params.psnr_target > 0) { param.target_mode = sjpeg::EncoderParam::TARGET_PSNR; param.target_value = params.psnr_target; } if (param.target_mode != sjpeg::EncoderParam::TARGET_NONE) { param.passes = params.search_max_iters; param.tolerance = params.search_tolerance; param.qmin = params.search_q_min; param.qmax = params.search_q_max; hook.reset(new MySearchHook()); hook->ReadBaseTables(params.custom_base_quant_fn); hook->q_start = params.search_q_start; hook->q_precision = params.search_q_precision; hook->first_iter_slope = params.search_first_iter_slope; param.search_hook = hook.get(); } size_t stride = info.xsize * 3; const uint8_t* pixels = reinterpret_cast(image.pixels()); std::string output; JXL_RETURN_IF_ERROR( sjpeg::Encode(pixels, image.xsize, image.ysize, stride, param, &output)); bytes->assign( reinterpret_cast(output.data()), reinterpret_cast(output.data() + output.size())); return true; #endif } Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info, const std::vector& icc, std::vector exif, JpegEncoder encoder, const JpegParams& params, ThreadPool* pool, std::vector* bytes) { if (params.quality > 100) { return JXL_FAILURE("please specify a 0-100 JPEG quality"); } switch (encoder) { case JpegEncoder::kLibJpeg: JXL_RETURN_IF_ERROR( EncodeWithLibJpeg(image, info, icc, std::move(exif), params, bytes)); break; case JpegEncoder::kSJpeg: JXL_RETURN_IF_ERROR( EncodeWithSJpeg(image, info, icc, std::move(exif), params, bytes)); break; default: return JXL_FAILURE("tried to use an unknown JPEG encoder"); } return true; } class JPEGEncoder : public Encoder { std::vector AcceptedFormats() const override { std::vector formats; for (const uint32_t num_channels : {1, 2, 3, 4}) { for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, /*data_type=*/JXL_TYPE_UINT8, /*endianness=*/endianness, /*align=*/0}); } formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, /*data_type=*/JXL_TYPE_UINT16, /*endianness=*/JXL_BIG_ENDIAN, /*align=*/0}); } return formats; } Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, ThreadPool* pool) const override { JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg; JpegParams params; for (const auto& it : options()) { if (it.first == "q") { std::istringstream is(it.second); JXL_RETURN_IF_ERROR(static_cast(is >> params.quality)); } else if (it.first == "libjpeg_quality") { std::istringstream is(it.second); JXL_RETURN_IF_ERROR(static_cast(is >> params.libjpeg_quality)); } else if (it.first == "chroma_subsampling") { params.chroma_subsampling = it.second; } else if (it.first == "libjpeg_chroma_subsampling") { params.libjpeg_chroma_subsampling = it.second; } else if (it.first == "jpeg_encoder") { if (it.second == "libjpeg") { jpeg_encoder = JpegEncoder::kLibJpeg; } else if (it.second == "sjpeg") { jpeg_encoder = JpegEncoder::kSJpeg; } else { return JXL_FAILURE("unknown jpeg encoder \"%s\"", it.second.c_str()); } } else if (it.first == "progressive") { std::istringstream is(it.second); JXL_RETURN_IF_ERROR(static_cast(is >> params.progressive_id)); } else if (it.first == "optimize" && it.second == "OFF") { params.optimize_coding = false; } else if (it.first == "adaptive_q" && it.second == "OFF") { params.enable_adaptive_quant = false; } else if (it.first == "psnr") { params.psnr_target = std::stof(it.second); } else if (it.first == "base_quant_fn") { params.custom_base_quant_fn = it.second; } else if (it.first == "search_q_start") { params.search_q_start = std::stof(it.second); } else if (it.first == "search_q_min") { params.search_q_min = std::stof(it.second); } else if (it.first == "search_q_max") { params.search_q_max = std::stof(it.second); } else if (it.first == "search_max_iters") { params.search_max_iters = std::stoi(it.second); } else if (it.first == "search_tolerance") { params.search_tolerance = std::stof(it.second); } else if (it.first == "search_q_precision") { params.search_q_precision = std::stof(it.second); } else if (it.first == "search_first_iter_slope") { params.search_first_iter_slope = std::stof(it.second); } } params.is_xyb = (ppf.color_encoding.color_space == JXL_COLOR_SPACE_XYB); encoded_image->bitstreams.clear(); encoded_image->bitstreams.reserve(ppf.frames.size()); for (const auto& frame : ppf.frames) { JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); encoded_image->bitstreams.emplace_back(); JXL_RETURN_IF_ERROR(EncodeImageJPG( frame.color, ppf.info, ppf.icc, ppf.metadata.exif, jpeg_encoder, params, pool, &encoded_image->bitstreams.back())); } return true; } }; } // namespace #endif std::unique_ptr GetJPEGEncoder() { #if JPEGXL_ENABLE_JPEG return jxl::make_unique(); #else return nullptr; #endif } } // namespace extras } // namespace jxl