/src/libjxl/lib/extras/enc/exr.cc
Line | Count | Source |
1 | | // Copyright (c) the JPEG XL Project Authors. All rights reserved. |
2 | | // |
3 | | // Use of this source code is governed by a BSD-style |
4 | | // license that can be found in the LICENSE file. |
5 | | |
6 | | #include "lib/extras/enc/exr.h" |
7 | | |
8 | | #include <memory> |
9 | | |
10 | | // IWYU pragma: no_include "ImathConfig.h" |
11 | | // IWYU pragma: no_include "ImfChromaticities.h" |
12 | | // IWYU pragma: no_include "ImfHeader.h" |
13 | | // IWYU pragma: no_include "OpenEXRConfig.h" |
14 | | #include "lib/extras/enc/encode.h" |
15 | | |
16 | | #if !JPEGXL_ENABLE_EXR |
17 | | |
18 | | namespace jxl { |
19 | | namespace extras { |
20 | 0 | std::unique_ptr<Encoder> GetEXREncoder() { return nullptr; } |
21 | | } // namespace extras |
22 | | } // namespace jxl |
23 | | |
24 | | #else // JPEGXL_ENABLE_EXR |
25 | | |
26 | | #include <ImathVec.h> |
27 | | #include <ImfIO.h> |
28 | | #include <ImfNamespace.h> // IWYU pragma: keep |
29 | | #include <ImfRgba.h> |
30 | | #include <ImfRgbaFile.h> |
31 | | #include <ImfStandardAttributes.h> |
32 | | #include <ImfThreading.h> |
33 | | #include <jxl/codestream_header.h> |
34 | | #include <jxl/color_encoding.h> |
35 | | #include <jxl/types.h> |
36 | | |
37 | | #include <algorithm> |
38 | | #include <cstddef> |
39 | | #include <cstdint> |
40 | | #include <cstring> |
41 | | #include <utility> |
42 | | #include <vector> |
43 | | |
44 | | #include "lib/extras/packed_image.h" |
45 | | #include "lib/jxl/base/byte_order.h" |
46 | | #include "lib/jxl/base/common.h" |
47 | | #include "lib/jxl/base/compiler_specific.h" |
48 | | #include "lib/jxl/base/data_parallel.h" |
49 | | #include "lib/jxl/base/status.h" |
50 | | |
51 | | namespace jxl { |
52 | | namespace extras { |
53 | | namespace { |
54 | | |
55 | | namespace OpenEXR = OPENEXR_IMF_NAMESPACE; |
56 | | namespace Imath = IMATH_NAMESPACE; |
57 | | |
58 | | // OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using |
59 | | // uint64_t as recommended causes build failures with previous OpenEXR versions |
60 | | // on macOS, where the definition for OpenEXR::Int64 was actually not equivalent |
61 | | // to uint64_t. This alternative should work in all cases. |
62 | | using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg()); |
63 | | |
64 | | class InMemoryOStream : public OpenEXR::OStream { |
65 | | public: |
66 | | // `bytes` must outlive the InMemoryOStream. |
67 | | explicit InMemoryOStream(std::vector<uint8_t>* const bytes) |
68 | | : OStream(/*fileName=*/""), bytes_(*bytes) {} |
69 | | |
70 | | void write(const char c[], const int n) override { |
71 | | if (bytes_.size() < pos_ + n) { |
72 | | bytes_.resize(pos_ + n); |
73 | | } |
74 | | std::copy_n(c, n, bytes_.begin() + pos_); |
75 | | pos_ += n; |
76 | | } |
77 | | |
78 | | ExrInt64 tellp() override { return pos_; } |
79 | | void seekp(const ExrInt64 pos) override { |
80 | | if (bytes_.size() + 1 < pos) { |
81 | | bytes_.resize(pos - 1); |
82 | | } |
83 | | pos_ = pos; |
84 | | } |
85 | | |
86 | | private: |
87 | | std::vector<uint8_t>& bytes_; |
88 | | size_t pos_ = 0; |
89 | | }; |
90 | | |
91 | | // Loads a Big-Endian float |
92 | | float LoadBEFloat(const uint8_t* p) { |
93 | | uint32_t u = LoadBE32(p); |
94 | | float result; |
95 | | memcpy(&result, &u, 4); |
96 | | return result; |
97 | | } |
98 | | |
99 | | // Loads a Little-Endian float |
100 | | float LoadLEFloat(const uint8_t* p) { |
101 | | uint32_t u = LoadLE32(p); |
102 | | float result; |
103 | | memcpy(&result, &u, 4); |
104 | | return result; |
105 | | } |
106 | | |
107 | | Status EncodeImageEXR(const PackedImage& image, const JxlBasicInfo& info, |
108 | | const JxlColorEncoding& c_enc, ThreadPool* pool, |
109 | | std::vector<uint8_t>* bytes) { |
110 | | OpenEXR::setGlobalThreadCount(0); |
111 | | |
112 | | const size_t xsize = image.xsize; |
113 | | const size_t ysize = image.ysize; |
114 | | const bool has_alpha = info.alpha_bits > 0; |
115 | | const bool alpha_is_premultiplied = FROM_JXL_BOOL(info.alpha_premultiplied); |
116 | | |
117 | | if (info.num_color_channels != 3) { |
118 | | return JXL_FAILURE("OpenEXR encoding: expected 3 color channels, got %u", |
119 | | static_cast<unsigned>(info.num_color_channels)); |
120 | | } |
121 | | if (c_enc.color_space != JXL_COLOR_SPACE_RGB && |
122 | | c_enc.color_space != JXL_COLOR_SPACE_GRAY) { |
123 | | return JXL_FAILURE( |
124 | | "OpenEXR encoding: expected RGB (%d) or grayscale (%d) colorspace, got " |
125 | | "%d", |
126 | | static_cast<int>(JXL_COLOR_SPACE_RGB), |
127 | | static_cast<int>(JXL_COLOR_SPACE_GRAY), |
128 | | static_cast<int>(c_enc.color_space)); |
129 | | } |
130 | | if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_LINEAR) { |
131 | | return JXL_FAILURE( |
132 | | "OpenEXR encoding: expected linear transfer function (%d), got %d", |
133 | | static_cast<int>(JXL_TRANSFER_FUNCTION_LINEAR), |
134 | | static_cast<int>(c_enc.transfer_function)); |
135 | | } |
136 | | |
137 | | const size_t num_channels = 3 + (has_alpha ? 1 : 0); |
138 | | const JxlPixelFormat format = image.format; |
139 | | |
140 | | if (format.data_type != JXL_TYPE_FLOAT) { |
141 | | return JXL_FAILURE("Unsupported pixel format for OpenEXR output"); |
142 | | } |
143 | | |
144 | | const uint8_t* in = static_cast<const uint8_t*>(image.pixels()); |
145 | | size_t in_stride = num_channels * 4 * xsize; |
146 | | |
147 | | OpenEXR::Header header(xsize, ysize); |
148 | | if (c_enc.color_space == JXL_COLOR_SPACE_RGB) { |
149 | | OpenEXR::Chromaticities chromaticities; |
150 | | chromaticities.red = |
151 | | Imath::V2f(c_enc.primaries_red_xy[0], c_enc.primaries_red_xy[1]); |
152 | | chromaticities.green = |
153 | | Imath::V2f(c_enc.primaries_green_xy[0], c_enc.primaries_green_xy[1]); |
154 | | chromaticities.blue = |
155 | | Imath::V2f(c_enc.primaries_blue_xy[0], c_enc.primaries_blue_xy[1]); |
156 | | chromaticities.white = |
157 | | Imath::V2f(c_enc.white_point_xy[0], c_enc.white_point_xy[1]); |
158 | | OpenEXR::addChromaticities(header, chromaticities); |
159 | | } |
160 | | OpenEXR::addWhiteLuminance(header, info.intensity_target); |
161 | | |
162 | | auto loadFloat = |
163 | | format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat : LoadLEFloat; |
164 | | auto loadAlpha = |
165 | | has_alpha ? loadFloat : [](const uint8_t* p) -> float { return 1.0f; }; |
166 | | |
167 | | // Ensure that the destructor of RgbaOutputFile has run before we look at the |
168 | | // size of `bytes`. |
169 | | { |
170 | | InMemoryOStream os(bytes); |
171 | | OpenEXR::RgbaOutputFile output( |
172 | | os, header, |
173 | | c_enc.color_space == JXL_COLOR_SPACE_GRAY |
174 | | ? (has_alpha ? OpenEXR::WRITE_YA : OpenEXR::WRITE_Y) |
175 | | : (has_alpha ? OpenEXR::WRITE_RGBA : OpenEXR::WRITE_RGB)); |
176 | | // How many rows to write at once. Again, the OpenEXR documentation |
177 | | // recommends writing the whole image in one call. |
178 | | const int y_chunk_size = ysize; |
179 | | std::vector<OpenEXR::Rgba> output_rows(xsize * y_chunk_size); |
180 | | |
181 | | for (size_t start_y = 0; start_y < ysize; start_y += y_chunk_size) { |
182 | | // Inclusive. |
183 | | const size_t end_y = std::min(start_y + y_chunk_size - 1, ysize - 1); |
184 | | output.setFrameBuffer(output_rows.data() - start_y * xsize, |
185 | | /*xStride=*/1, /*yStride=*/xsize); |
186 | | for (size_t y = start_y; y <= end_y; ++y) { |
187 | | const uint8_t* in_row = &in[(y - start_y) * in_stride]; |
188 | | OpenEXR::Rgba* const JXL_RESTRICT row_data = |
189 | | &output_rows[(y - start_y) * xsize]; |
190 | | for (size_t x = 0; x < xsize; ++x) { |
191 | | const uint8_t* in_pixel = &in_row[4 * num_channels * x]; |
192 | | float r = loadFloat(&in_pixel[0]); |
193 | | float g = loadFloat(&in_pixel[4]); |
194 | | float b = loadFloat(&in_pixel[8]); |
195 | | const float alpha = loadAlpha(&in_pixel[12]); |
196 | | if (!alpha_is_premultiplied) { |
197 | | r *= alpha; |
198 | | g *= alpha; |
199 | | b *= alpha; |
200 | | } |
201 | | row_data[x] = OpenEXR::Rgba(r, g, b, alpha); |
202 | | } |
203 | | } |
204 | | output.writePixels(/*numScanLines=*/end_y - start_y + 1); |
205 | | } |
206 | | } |
207 | | |
208 | | return true; |
209 | | } |
210 | | |
211 | | class EXREncoder : public Encoder { |
212 | | std::vector<JxlPixelFormat> AcceptedFormats() const override { |
213 | | std::vector<JxlPixelFormat> formats; |
214 | | for (const uint32_t num_channels : {3, 4}) { |
215 | | for (const JxlDataType data_type : {JXL_TYPE_FLOAT}) { |
216 | | for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { |
217 | | formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, |
218 | | /*data_type=*/data_type, |
219 | | /*endianness=*/endianness, |
220 | | /*align=*/0}); |
221 | | } |
222 | | } |
223 | | } |
224 | | return formats; |
225 | | } |
226 | | Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, |
227 | | ThreadPool* pool) const override { |
228 | | JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); |
229 | | encoded_image->icc.clear(); |
230 | | encoded_image->bitstreams.clear(); |
231 | | encoded_image->bitstreams.reserve(ppf.frames.size()); |
232 | | for (const auto& frame : ppf.frames) { |
233 | | JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); |
234 | | encoded_image->bitstreams.emplace_back(); |
235 | | JXL_RETURN_IF_ERROR(EncodeImageEXR(frame.color, ppf.info, |
236 | | ppf.color_encoding, pool, |
237 | | &encoded_image->bitstreams.back())); |
238 | | } |
239 | | return true; |
240 | | } |
241 | | }; |
242 | | |
243 | | } // namespace |
244 | | |
245 | | std::unique_ptr<Encoder> GetEXREncoder() { |
246 | | return jxl::make_unique<EXREncoder>(); |
247 | | } |
248 | | |
249 | | } // namespace extras |
250 | | } // namespace jxl |
251 | | |
252 | | #endif // JPEGXL_ENABLE_EXR |