// 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/exr.h" #if JPEGXL_ENABLE_EXR #include #include #include #include #endif #include #include #include "lib/extras/packed_image.h" #include "lib/jxl/base/byte_order.h" namespace jxl { namespace extras { #if JPEGXL_ENABLE_EXR namespace { namespace OpenEXR = OPENEXR_IMF_NAMESPACE; namespace Imath = IMATH_NAMESPACE; // OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using // uint64_t as recommended causes build failures with previous OpenEXR versions // on macOS, where the definition for OpenEXR::Int64 was actually not equivalent // to uint64_t. This alternative should work in all cases. using ExrInt64 = decltype(std::declval().tellg()); class InMemoryOStream : public OpenEXR::OStream { public: // `bytes` must outlive the InMemoryOStream. explicit InMemoryOStream(std::vector* const bytes) : OStream(/*fileName=*/""), bytes_(*bytes) {} void write(const char c[], const int n) override { if (bytes_.size() < pos_ + n) { bytes_.resize(pos_ + n); } std::copy_n(c, n, bytes_.begin() + pos_); pos_ += n; } ExrInt64 tellp() override { return pos_; } void seekp(const ExrInt64 pos) override { if (bytes_.size() + 1 < pos) { bytes_.resize(pos - 1); } pos_ = pos; } private: std::vector& bytes_; size_t pos_ = 0; }; // Loads a Big-Endian float float LoadBEFloat(const uint8_t* p) { uint32_t u = LoadBE32(p); float result; memcpy(&result, &u, 4); return result; } // Loads a Little-Endian float float LoadLEFloat(const uint8_t* p) { uint32_t u = LoadLE32(p); float result; memcpy(&result, &u, 4); return result; } Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info, const JxlColorEncoding& c_enc, ThreadPool* pool, std::vector* bytes) { OpenEXR::setGlobalThreadCount(0); const size_t xsize = info.xsize; const size_t ysize = info.ysize; const bool has_alpha = info.alpha_bits > 0; const bool alpha_is_premultiplied = FROM_JXL_BOOL(info.alpha_premultiplied); if (info.num_color_channels != 3 || c_enc.color_space != JXL_COLOR_SPACE_RGB || c_enc.transfer_function != JXL_TRANSFER_FUNCTION_LINEAR) { return JXL_FAILURE("Unsupported color encoding for OpenEXR output."); } const size_t num_channels = 3 + (has_alpha ? 1 : 0); const JxlPixelFormat format = image.format; if (format.data_type != JXL_TYPE_FLOAT) { return JXL_FAILURE("Unsupported pixel format for OpenEXR output"); } const uint8_t* in = reinterpret_cast(image.pixels()); size_t in_stride = num_channels * 4 * xsize; OpenEXR::Header header(xsize, ysize); OpenEXR::Chromaticities chromaticities; chromaticities.red = Imath::V2f(c_enc.primaries_red_xy[0], c_enc.primaries_red_xy[1]); chromaticities.green = Imath::V2f(c_enc.primaries_green_xy[0], c_enc.primaries_green_xy[1]); chromaticities.blue = Imath::V2f(c_enc.primaries_blue_xy[0], c_enc.primaries_blue_xy[1]); chromaticities.white = Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]); OpenEXR::addChromaticities(header, chromaticities); OpenEXR::addWhiteLuminance(header, info.intensity_target); auto loadFloat = format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat; auto loadAlpha = has_alpha ? loadFloat : [](const uint8_t* p) -> float { return 1.0f; }; // Ensure that the destructor of RgbaOutputFile has run before we look at the // size of `bytes`. { InMemoryOStream os(bytes); OpenEXR::RgbaOutputFile output( os, header, has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB); // How many rows to write at once. Again, the OpenEXR documentation // recommends writing the whole image in one call. const int y_chunk_size = ysize; std::vector output_rows(xsize * y_chunk_size); for (size_t start_y = 0; start_y < ysize; start_y += y_chunk_size) { // Inclusive. const size_t end_y = std::min(start_y + y_chunk_size - 1, ysize - 1); output.setFrameBuffer(output_rows.data() - start_y * xsize, /*xStride=*/1, /*yStride=*/xsize); for (size_t y = start_y; y <= end_y; ++y) { const uint8_t* in_row = &in[(y - start_y) * in_stride]; OpenEXR::Rgba* const JXL_RESTRICT row_data = &output_rows[(y - start_y) * xsize]; for (size_t x = 0; x < xsize; ++x) { const uint8_t* in_pixel = &in_row[4 * num_channels * x]; float r = loadFloat(&in_pixel[0]); float g = loadFloat(&in_pixel[4]); float b = loadFloat(&in_pixel[8]); const float alpha = loadAlpha(&in_pixel[12]); if (!alpha_is_premultiplied) { r *= alpha; g *= alpha; b *= alpha; } row_data[x] = OpenEXR::Rgba(r, g, b, alpha); } } output.writePixels(/*numScanLines=*/end_y - start_y + 1); } } return true; } class EXREncoder : public Encoder { std::vector AcceptedFormats() const override { std::vector formats; for (const uint32_t num_channels : {1, 2, 3, 4}) { for (const JxlDataType data_type : {JXL_TYPE_FLOAT}) { for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, /*data_type=*/data_type, /*endianness=*/endianness, /*align=*/0}); } } } return formats; } Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, ThreadPool* pool) const override { JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); encoded_image->icc.clear(); 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(EncodeImageEXR(frame.color, ppf.info, ppf.color_encoding, pool, &encoded_image->bitstreams.back())); } return true; } }; } // namespace #endif std::unique_ptr GetEXREncoder() { #if JPEGXL_ENABLE_EXR return jxl::make_unique(); #else return nullptr; #endif } } // namespace extras } // namespace jxl