#include "fgc/JpegXlEncoder.h" #include "fgc/Logger.h" #include #include #include #include #include #include #include namespace fgc { bool JpegXlEncoder::encodeToFile(const std::string& path, const void* img, int width, int height, int channels, int bits, float distance, int effort, int threads) { if (!img || width <= 0 || height <= 0 || (channels != 1 && channels != 3) || bits < 8 || bits > 16 || distance < 0.0f) { LOG_ERROR << "JpegXlEncoder: bad arguments (" << width << 'x' << height << ", " << channels << " channels, " << bits << " bits, distance " << distance << ')'; return false; } const bool lossless = distance == 0.0f; const bool wide = bits > 8; auto enc = JxlEncoderMake(nullptr); JxlThreadParallelRunnerPtr runner; if (threads > 0) { runner = JxlThreadParallelRunnerMake(nullptr, static_cast(threads)); if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(), JxlThreadParallelRunner, runner.get())) { LOG_ERROR << "JxlEncoderSetParallelRunner failed"; return false; } } JxlPixelFormat pixel_format = {static_cast(channels), wide ? JXL_TYPE_UINT16 : JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; JxlBasicInfo basic_info; JxlEncoderInitBasicInfo(&basic_info); basic_info.xsize = width; basic_info.ysize = height; basic_info.alpha_bits = 0; basic_info.bits_per_sample = static_cast(bits); basic_info.num_color_channels = channels; // Lossless requires the original colour space; with it off, libjxl encodes // through its lossy XYB space, reports success, and the pixels come back // changed (known-issues #20). Lossy keeps XYB, as the program always has. basic_info.uses_original_profile = lossless ? JXL_TRUE : JXL_FALSE; if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc.get(), &basic_info)) { LOG_ERROR << "JxlEncoderSetBasicInfo failed"; return false; } JxlColorEncoding color_encoding = {}; JxlColorEncodingSetToSRGB(&color_encoding, /*is_gray=*/channels < 3); if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) { LOG_ERROR << "JxlEncoderSetColorEncoding failed"; return false; } JxlEncoderFrameSettings* fs = JxlEncoderFrameSettingsCreate(enc.get(), nullptr); JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_EFFORT, effort); JxlEncoderFrameSettingsSetOption(fs, JXL_ENC_FRAME_SETTING_DECODING_SPEED, 0); if (lossless) { JxlEncoderSetFrameDistance(fs, 0); JxlEncoderSetFrameLossless(fs, JXL_TRUE); } else { JxlEncoderSetFrameLossless(fs, JXL_FALSE); JxlEncoderSetFrameDistance(fs, distance); } if (wide) { // The 16-bit words hold `bits`-bit values: read them at the header's // depth, not as the full 0..65535 range. JxlBitDepth depth = {JXL_BIT_DEPTH_FROM_CODESTREAM, 0, 0}; if (JXL_ENC_SUCCESS != JxlEncoderSetFrameBitDepth(fs, &depth)) { LOG_ERROR << "JxlEncoderSetFrameBitDepth failed"; return false; } } const size_t byte_size = static_cast(width) * height * channels * (wide ? 2 : 1); if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(fs, &pixel_format, img, byte_size)) { LOG_ERROR << "JxlEncoderAddImageFrame failed"; return false; } JxlEncoderCloseInput(enc.get()); std::vector compressed(64); uint8_t* next_out = compressed.data(); size_t avail_out = compressed.size(); JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT; while (result == JXL_ENC_NEED_MORE_OUTPUT) { result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); if (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; } } if (JXL_ENC_SUCCESS != result) { LOG_ERROR << "JxlEncoderProcessOutput failed"; return false; } compressed.resize(next_out - compressed.data()); FILE* file = std::fopen(path.c_str(), "wb"); if (!file) { LOG_ERROR << "Could not open " << path << " for writing"; return false; } bool ok = std::fwrite(compressed.data(), 1, compressed.size(), file) == compressed.size(); if (std::fclose(file) != 0) ok = false; if (!ok) { LOG_ERROR << "Could not write " << path; std::remove(path.c_str()); } return ok; } } // namespace fgc