Coverage Report

Created: 2026-08-14 08:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/extras/dec/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/dec/exr.h"
7
8
#include <jxl/codestream_header.h>  // JXL_CHANNEL_OPTIONAL
9
10
#include <cstdint>
11
12
#include "lib/extras/dec/color_hints.h"
13
#include "lib/extras/packed_image.h"
14
#include "lib/extras/size_constraints.h"
15
#include "lib/jxl/base/span.h"
16
#include "lib/jxl/base/status.h"
17
18
#if !JPEGXL_ENABLE_EXR
19
20
namespace jxl {
21
namespace extras {
22
0
bool CanDecodeEXR() { return false; }
23
24
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
25
                      PackedPixelFile* ppf,
26
1
                      const SizeConstraints* constraints) {
27
1
  (void)bytes;
28
1
  (void)color_hints;
29
1
  (void)ppf;
30
1
  (void)constraints;
31
1
  return JXL_FAILURE("EXR is not supported");
32
1
}
33
}  // namespace extras
34
}  // namespace jxl
35
36
#else  // JPEGXL_ENABLE_EXR
37
38
#include <ImfChannelList.h>
39
#include <ImfFrameBuffer.h>
40
#include <ImfIO.h>
41
#include <ImfInputFile.h>
42
#include <ImfStandardAttributes.h>
43
#include <OpenEXRConfig.h>
44
#include <jxl/color_encoding.h>
45
#include <jxl/types.h>
46
47
#include <algorithm>
48
#include <cstddef>
49
#include <cstring>
50
#include <memory>
51
#include <set>
52
#include <string>
53
#include <utility>
54
#include <vector>
55
56
#include "lib/jxl/base/common.h"
57
#include "lib/jxl/base/compiler_specific.h"
58
59
#ifdef __EXCEPTIONS
60
#include <IexBaseExc.h>
61
#define JXL_EXR_THROW_LENGTH_ERROR(M) throw Iex::InputExc(M);
62
#else  // __EXCEPTIONS
63
#define JXL_EXR_THROW_LENGTH_ERROR(M) JXL_CRASH()
64
#endif  // __EXCEPTIONS
65
66
namespace jxl {
67
namespace extras {
68
69
namespace {
70
71
namespace OpenEXR = OPENEXR_IMF_NAMESPACE;
72
73
// OpenEXR::Int64 is deprecated in favor of using uint64_t directly, but using
74
// uint64_t as recommended causes build failures with previous OpenEXR versions
75
// on macOS, where the definition for OpenEXR::Int64 was actually not equivalent
76
// to uint64_t. This alternative should work in all cases.
77
using ExrInt64 = decltype(std::declval<OpenEXR::IStream>().tellg());
78
79
class InMemoryIStream : public OpenEXR::IStream {
80
 public:
81
  // The data pointed to by `bytes` must outlive the InMemoryIStream.
82
  explicit InMemoryIStream(const Span<const uint8_t> bytes)
83
      : IStream(/*fileName=*/""), bytes_(bytes) {}
84
85
  bool isMemoryMapped() const override { return true; }
86
  char* readMemoryMapped(const int n) override {
87
    if (pos_ + n < pos_) {
88
      JXL_EXR_THROW_LENGTH_ERROR("Overflow");
89
    }
90
    if (pos_ + n > bytes_.size()) {
91
      JXL_EXR_THROW_LENGTH_ERROR("Read past end of file");
92
    }
93
    char* const result =
94
        const_cast<char*>(reinterpret_cast<const char*>(bytes_.data() + pos_));
95
    pos_ += n;
96
    return result;
97
  }
98
  bool read(char c[/*n*/], int n) override {
99
    // That is not stated in documentation, but the OpenEXR code expects that
100
    // when requested amount is not accessible and exception is thrown, all
101
    // the accessible data is read.
102
    if (pos_ + n < pos_) {
103
      JXL_EXR_THROW_LENGTH_ERROR("Overflow");
104
    }
105
    if (pos_ + n > bytes_.size()) {
106
      int can_read = static_cast<int>(bytes_.size() - pos_);
107
      std::copy_n(readMemoryMapped(can_read), can_read, c);
108
      JXL_EXR_THROW_LENGTH_ERROR("Read past end of file");
109
    } else {
110
      std::copy_n(readMemoryMapped(n), n, c);
111
    }
112
    return pos_ < bytes_.size();
113
  }
114
115
  ExrInt64 tellg() override { return pos_; }
116
  void seekg(const ExrInt64 pos) override {
117
    if (pos >= bytes_.size()) {
118
      JXL_EXR_THROW_LENGTH_ERROR("Seeks past end of file");
119
    }
120
    pos_ = pos;
121
  }
122
123
 private:
124
  const Span<const uint8_t> bytes_;
125
  size_t pos_ = 0;
126
};
127
128
}  // namespace
129
130
bool CanDecodeEXR() { return true; }
131
132
static std::string FindColorLayerPrefix(const OpenEXR::ChannelList& channels) {
133
  // check if just R,G,B channels exist; use those if present
134
  if (channels.findChannel("R") != nullptr &&
135
      channels.findChannel("G") != nullptr &&
136
      channels.findChannel("B") != nullptr) {
137
    return "";
138
  }
139
140
  // check channels for presence of R,G,B with common prefix ("layer name"),
141
  // use the first one that is present
142
  for (OpenEXR::ChannelList::ConstIterator it = channels.begin();
143
       it != channels.end(); ++it) {
144
    std::string name = it.name();
145
    size_t dotIndex = name.rfind('.');
146
    if (dotIndex == std::string::npos) continue;
147
    std::string suffix = name.substr(dotIndex + 1);
148
    if (suffix != "R" && suffix != "G" && suffix != "B") continue;
149
    std::string prefix = name.substr(0, dotIndex + 1);
150
    if (channels.findChannel((prefix + "R").c_str()) != nullptr &&
151
        channels.findChannel((prefix + "G").c_str()) != nullptr &&
152
        channels.findChannel((prefix + "B").c_str()) != nullptr) {
153
      return prefix;
154
    }
155
  }
156
157
  return "";
158
}
159
160
Status DecodeImageEXR(Span<const uint8_t> bytes, const ColorHints& color_hints,
161
                      PackedPixelFile* ppf,
162
                      const SizeConstraints* constraints) {
163
  InMemoryIStream is(bytes);
164
165
#ifdef __EXCEPTIONS
166
  std::unique_ptr<OpenEXR::InputFile> input_ptr;
167
  try {
168
    input_ptr = jxl::make_unique<OpenEXR::InputFile>(is);
169
  } catch (...) {
170
    // silently return false if it is not an EXR file
171
    return false;
172
  }
173
  OpenEXR::InputFile& input = *input_ptr;
174
#else
175
  OpenEXR::InputFile input(is);
176
#endif
177
178
  const OpenEXR::Header& header = input.header();
179
  const OpenEXR::ChannelList& channels = header.channels();
180
181
  // we don't support subsampled or UINT channels yet
182
  // TODO: support common cases of subsampling (2x, 4x)
183
  for (OpenEXR::ChannelList::ConstIterator it = channels.begin();
184
       it != channels.end(); ++it) {
185
    const OpenEXR::Channel& ch = it.channel();
186
    if (ch.type == OpenEXR::UINT) {
187
      return JXL_FAILURE("OpenEXR files with UINT channels are not supported");
188
    }
189
    if (ch.xSampling != 1 || ch.ySampling != 1) {
190
      return JXL_FAILURE(
191
          "OpenEXR files sub-sampled channels are not supported");
192
    }
193
  }
194
195
  const std::string color_prefix = FindColorLayerPrefix(channels);
196
  const std::string ch_name_r = color_prefix + "R";
197
  const OpenEXR::Channel* ch_r = channels.findChannel(ch_name_r.c_str());
198
  const std::string ch_name_g = color_prefix + "G";
199
  const OpenEXR::Channel* ch_g = channels.findChannel(ch_name_g.c_str());
200
  const std::string ch_name_b = color_prefix + "B";
201
  const OpenEXR::Channel* ch_b = channels.findChannel(ch_name_b.c_str());
202
  const std::string ch_name_a = color_prefix + "A";
203
  const OpenEXR::Channel* ch_a = channels.findChannel(ch_name_a.c_str());
204
  // If we don't have RGB (same type) channels, we'll treat the first
205
  // channel as grayscale.
206
  bool has_rgb = (ch_r != nullptr) && (ch_g != nullptr) && (ch_b != nullptr);
207
  bool is_gray = !has_rgb;
208
  const OpenEXR::Channel* ch_gray =
209
      is_gray ? &channels.begin().channel() : nullptr;
210
  const std::string ch_name_gray = is_gray ? channels.begin().name() : "";
211
212
  const auto color_type = (is_gray ? ch_gray : ch_r)->type;
213
  if (has_rgb) {
214
    if (ch_g->type != color_type || ch_b->type != color_type) {
215
      return JXL_FAILURE(
216
          "OpenEXR color channels with different types are not supported yet");
217
    }
218
  }
219
220
  bool has_alpha = (ch_a != nullptr) && (ch_a != ch_gray);
221
  if (has_alpha) {
222
    if (ch_a->type != color_type) {
223
      return JXL_FAILURE(
224
          "OpenEXR color channels with different types are not supported yet");
225
    }
226
  }
227
228
  const float intensity_target =
229
      OpenEXR::hasWhiteLuminance(header) ? OpenEXR::whiteLuminance(header) : 0;
230
231
  const Imath::Box2i display_window = header.displayWindow();
232
  const Imath::Box2i data_window = header.dataWindow();
233
  // display_window / data_window bounds come straight from the EXR file header
234
  // and are not otherwise constrained: an arbitrary range of ints is legal in
235
  // the file format. Reject coordinates outside +/-2^30 to keep the pointer
236
  // arithmetic below sane. JXL level 10 already caps image sizes at 2^30, so
237
  // this is not a real-world limitation. The inclusive window sizes are still
238
  // computed in 64 bits, because the span of two in-range coordinates can
239
  // reach 2^31, which does not fit in `int`.
240
  constexpr int kEXRCoordBound = 1 << 30;
241
  auto OutOfRange = [](int v) {
242
    return v < -kEXRCoordBound || v > kEXRCoordBound;
243
  };
244
  if (OutOfRange(display_window.min.x) || OutOfRange(display_window.max.x) ||
245
      OutOfRange(display_window.min.y) || OutOfRange(display_window.max.y) ||
246
      OutOfRange(data_window.min.x) || OutOfRange(data_window.max.x) ||
247
      OutOfRange(data_window.min.y) || OutOfRange(data_window.max.y)) {
248
    return JXL_FAILURE("EXR: window coordinates out of range");
249
  }
250
  // TODO(eustas): empty data_window could be valid use case.
251
  if (display_window.isEmpty() || data_window.isEmpty()) {
252
    return JXL_FAILURE("EXR: empty window");
253
  }
254
  // Size is computed as max - min, but both bounds are inclusive. Compute in
255
  // 64 bits: `Box2i::size()` subtracts two `int` coordinates, which overflows
256
  // when the (in-range) span reaches 2^31.
257
  const int64_t image_width =
258
      static_cast<int64_t>(display_window.max.x) - display_window.min.x + 1;
259
  const int64_t image_height =
260
      static_cast<int64_t>(display_window.max.y) - display_window.min.y + 1;
261
262
  if (!VerifyDimensions<uint32_t>(constraints, image_width, image_height)) {
263
    return JXL_FAILURE("image too big");
264
  }
265
266
  // Apply the same constraints to data_window, since its dimensions drive
267
  // input buffer allocations and per-row pointer arithmetic below.
268
  const int64_t data_width =
269
      static_cast<int64_t>(data_window.max.x) - data_window.min.x + 1;
270
  const int64_t data_height =
271
      static_cast<int64_t>(data_window.max.y) - data_window.min.y + 1;
272
  if (!VerifyDimensions<uint32_t>(constraints, data_width, data_height)) {
273
    return JXL_FAILURE("EXR: data_window too big");
274
  }
275
276
  // https://www.openexr.com/documentation/ReadingAndWritingImageFiles.pdf
277
  // recommends reading the whole file at once.
278
  size_t num_pixels;
279
  // Width must correspond to data scanlines in file.
280
  // TODO(eustas): if projection of data_window on X axis is inside of
281
  // projection of display_window, then we can avoid extra memcpy.
282
  if (!SafeMul(image_height, data_width, num_pixels)) {
283
    return JXL_FAILURE("EXR: image too big");
284
  }
285
286
  // Intersect data and display window.
287
  const int x1 = std::max(data_window.min.x, display_window.min.x);
288
  const int x2 = std::min(data_window.max.x, display_window.max.x);
289
  const int y1 = std::max(data_window.min.y, display_window.min.y);
290
  const int y2 = std::min(data_window.max.y, display_window.max.y);
291
  const int x_span = x2 - x1 + 1;
292
  const int y_span = y2 - y1 + 1;
293
294
  const auto get_pixel_type = [](OpenEXR::PixelType t) -> JxlDataType {
295
    return (t == OpenEXR::HALF) ? JXL_TYPE_FLOAT16 : JXL_TYPE_FLOAT;
296
  };
297
298
  const auto get_pixel_stride = [](OpenEXR::PixelType t) -> size_t {
299
    return (t == OpenEXR::HALF) ? 2 : 4;
300
  };
301
302
  const auto get_bpp = [](OpenEXR::PixelType t) -> size_t {
303
    return (t == OpenEXR::HALF) ? 16 : 32;
304
  };
305
306
  const auto get_exponent_bits = [](OpenEXR::PixelType t) -> size_t {
307
    return (t == OpenEXR::HALF) ? 5 : 8;
308
  };
309
310
  uint32_t num_color_channels = is_gray ? 1 : 3;
311
312
  ppf->info.xsize = image_width;
313
  ppf->info.ysize = image_height;
314
  ppf->info.num_color_channels = num_color_channels;
315
316
  const JxlPixelFormat format{
317
      /*num_channels=*/num_color_channels + (has_alpha ? 1u : 0u),
318
      /*data_type=*/get_pixel_type(color_type),
319
      /*endianness=*/JXL_NATIVE_ENDIAN,
320
      /*align=*/0,
321
  };
322
  ppf->frames.clear();
323
  // Allocates the frame buffer.
324
  {
325
    JXL_ASSIGN_OR_RETURN(
326
        PackedFrame frame,
327
        PackedFrame::Create(image_width, image_height, format));
328
    ppf->frames.emplace_back(std::move(frame));
329
  }
330
  auto& frame = ppf->frames.back();
331
332
  // Allocate extra channel images.
333
  std::set<const OpenEXR::Channel*> ec_set;
334
  std::vector<std::vector<char> > ec_data;
335
  for (OpenEXR::ChannelList::ConstIterator it = channels.begin();
336
       it != channels.end(); ++it) {
337
    const std::string name = it.name();
338
    // Skip {RGB|Gray}(A)
339
    if (is_gray && (name == ch_name_gray)) continue;
340
    if (has_alpha && (name == ch_name_a)) continue;
341
    if (has_rgb) {
342
      if ((name == ch_name_r) || (name == ch_name_g) || (name == ch_name_b)) {
343
        continue;
344
      }
345
    }
346
    const OpenEXR::Channel* ch = &it.channel();
347
    OpenEXR::PixelType t = ch->type;
348
349
    ec_set.insert(ch);
350
    size_t pixel_stride = get_pixel_stride(t);
351
    size_t volume;
352
    if (!SafeMul(pixel_stride, num_pixels, volume)) {
353
      return JXL_FAILURE("EXR: image too big");
354
    }
355
    std::vector<char> storage(volume);
356
    ec_data.emplace_back(std::move(storage));
357
358
    const JxlPixelFormat ec_format{/*num_channels=*/1,
359
                                   /*data_type=*/get_pixel_type(t),
360
                                   /*endianness=*/JXL_NATIVE_ENDIAN,
361
                                   /*align=*/0};
362
    JXL_ASSIGN_OR_RETURN(
363
        PackedImage ec,
364
        PackedImage::Create(image_width, image_height, ec_format));
365
    frame.extra_channels.emplace_back(std::move(ec));
366
    JXL_DASSERT(frame.extra_channels.back().pixel_stride() == pixel_stride);
367
368
    PackedExtraChannel pec = {};
369
    pec.ec_info.bits_per_sample = get_bpp(t);
370
    pec.ec_info.exponent_bits_per_sample = get_exponent_bits(t);
371
    // TODO: detect channel types (depth etc.) based on naming convention
372
    pec.ec_info.type = JXL_CHANNEL_OPTIONAL;
373
    pec.name = name;
374
    ppf->extra_channels_info.emplace_back(std::move(pec));
375
  }
376
  ppf->info.num_extra_channels =
377
      (has_alpha ? 1 : 0) + frame.extra_channels.size();
378
379
  const size_t color_channel_bytes = get_pixel_stride(color_type);
380
  const size_t color_pixel_bytes = color_channel_bytes * format.num_channels;
381
  size_t color_data_size;
382
  if (!SafeMul(color_pixel_bytes, num_pixels, color_data_size)) {
383
    return JXL_FAILURE("EXR: image too big");
384
  }
385
  // Interleaved RGB{A} / Gray{A}
386
  std::vector<char> color_data(color_data_size);
387
388
  // If intersection is empty, then image is just zeroes.
389
  if (x_span > 0 && y_span > 0) {
390
    // Setup framebuffer: color/grayscale
391
    OpenEXR::FrameBuffer fb;
392
    size_t x_stride = color_pixel_bytes;
393
    size_t y_stride = x_stride * static_cast<size_t>(data_width);
394
    char* virtual_image_origin = color_data.data();
395
    // Offset to match output to allocation start; when EXR puts pixel at
396
    // (data_window.min.x, y1) it goes to 0-th element.
397
    virtual_image_origin -=
398
        static_cast<ptrdiff_t>(y1) * static_cast<ptrdiff_t>(y_stride);
399
    virtual_image_origin -= static_cast<ptrdiff_t>(data_window.min.x) *
400
                            static_cast<ptrdiff_t>(x_stride);
401
    if (has_rgb) {
402
      fb.insert(ch_name_r.c_str(),
403
                OpenEXR::Slice(color_type,
404
                               virtual_image_origin + color_channel_bytes * 0,
405
                               x_stride, y_stride));
406
      fb.insert(ch_name_g.c_str(),
407
                OpenEXR::Slice(color_type,
408
                               virtual_image_origin + color_channel_bytes * 1,
409
                               x_stride, y_stride));
410
      fb.insert(ch_name_b.c_str(),
411
                OpenEXR::Slice(color_type,
412
                               virtual_image_origin + color_channel_bytes * 2,
413
                               x_stride, y_stride));
414
    } else {
415
      fb.insert(ch_name_gray.c_str(),
416
                OpenEXR::Slice(color_type,
417
                               virtual_image_origin + color_channel_bytes * 0,
418
                               x_stride, y_stride));
419
    }
420
421
    // Setup framebuffer: alpha
422
    if (has_alpha) {
423
      fb.insert(ch_name_a.c_str(),
424
                OpenEXR::Slice(color_type,
425
                               virtual_image_origin +
426
                                   color_channel_bytes * (has_rgb ? 3 : 1),
427
                               x_stride, y_stride));
428
    }
429
430
    // Setup framebuffer: extra channels
431
    size_t ec_data_idx = 0;
432
    for (OpenEXR::ChannelList::ConstIterator it = channels.begin();
433
         it != channels.end(); ++it) {
434
      const OpenEXR::Channel* ch = &it.channel();
435
      if (ec_set.find(ch) == ec_set.end()) {
436
        continue;
437
      }
438
      auto& ec = ec_data[ec_data_idx++];
439
      size_t ec_x_stride = get_pixel_stride(ch->type);
440
      size_t ec_y_stride = ec_x_stride * static_cast<size_t>(data_width);
441
442
      char* ec_virtual_image_origin = ec.data();
443
      // Offset to match output to allocation start; when EXR puts pixel at
444
      // (data_window.min.x, y1) it goes to 0-th element.
445
      ec_virtual_image_origin -=
446
          static_cast<ptrdiff_t>(y1) * static_cast<ptrdiff_t>(ec_y_stride);
447
      ec_virtual_image_origin -= static_cast<ptrdiff_t>(data_window.min.x) *
448
                                 static_cast<ptrdiff_t>(ec_x_stride);
449
450
      fb.insert(it.name(), OpenEXR::Slice(ch->type, ec_virtual_image_origin,
451
                                          ec_x_stride, ec_y_stride));
452
    }
453
454
    // Read EXR data
455
    input.setFrameBuffer(fb);
456
    input.readPixels(y1, y2);
457
458
    const int x_data = x1 - data_window.min.x;
459
    const int x_out = x1 - display_window.min.x;
460
    JXL_DASSERT(x_out >= 0);
461
    JXL_DASSERT(x_out + x_span <= image_width);
462
463
    // Copy read data into the result image.
464
    // TODO(eustas): should we deal with unpopulated pixels?
465
    for (int y = y1; y <= y2; ++y) {  // Scanline index
466
      const int y_data = y - y1;
467
      const int y_out = y - display_window.min.y;
468
      JXL_DASSERT(y_out >= 0);
469
      JXL_DASSERT(y_out < image_height);
470
      const char* const JXL_RESTRICT data_ptr =
471
          &color_data[x_data * color_pixel_bytes + y_data * y_stride];
472
      uint8_t* pixels = static_cast<uint8_t*>(frame.color.pixels());
473
      uint8_t* image_ptr =
474
          pixels + x_out * color_pixel_bytes + y_out * frame.color.stride;
475
      memcpy(image_ptr, data_ptr, x_span * color_pixel_bytes);
476
477
      for (size_t ec_idx = 0; ec_idx < frame.extra_channels.size(); ++ec_idx) {
478
        PackedImage& ec = frame.extra_channels[ec_idx];
479
        auto& data = ec_data[ec_idx];
480
        size_t ec_x_stride = ec.pixel_stride();
481
        size_t ec_y_stride = ec_x_stride * static_cast<size_t>(data_width);
482
        const char* const JXL_RESTRICT ec_data_ptr =
483
            &data[x_data * ec_x_stride + y_data * ec_y_stride];
484
        uint8_t* ec_pixels = static_cast<uint8_t*>(ec.pixels());
485
        uint8_t* ec_image_ptr =
486
            ec_pixels + x_out * ec_x_stride + y_out * ec.stride;
487
        memcpy(ec_image_ptr, ec_data_ptr, x_span * ec_x_stride);
488
      }
489
    }
490
  }
491
492
  ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
493
  ppf->color_encoding.color_space =
494
      has_rgb ? JXL_COLOR_SPACE_RGB : JXL_COLOR_SPACE_GRAY;
495
  ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB;
496
  ppf->color_encoding.white_point = JXL_WHITE_POINT_D65;
497
  if (OpenEXR::hasChromaticities(header)) {
498
    ppf->color_encoding.primaries = JXL_PRIMARIES_CUSTOM;
499
    ppf->color_encoding.white_point = JXL_WHITE_POINT_CUSTOM;
500
    const auto& chromaticities = OpenEXR::chromaticities(header);
501
    ppf->color_encoding.primaries_red_xy[0] = chromaticities.red.x;
502
    ppf->color_encoding.primaries_red_xy[1] = chromaticities.red.y;
503
    ppf->color_encoding.primaries_green_xy[0] = chromaticities.green.x;
504
    ppf->color_encoding.primaries_green_xy[1] = chromaticities.green.y;
505
    ppf->color_encoding.primaries_blue_xy[0] = chromaticities.blue.x;
506
    ppf->color_encoding.primaries_blue_xy[1] = chromaticities.blue.y;
507
    ppf->color_encoding.white_point_xy[0] = chromaticities.white.x;
508
    ppf->color_encoding.white_point_xy[1] = chromaticities.white.y;
509
  }
510
511
  // EXR uses binary16 or binary32 floating point format.
512
  ppf->info.bits_per_sample = get_bpp(color_type);
513
  ppf->info.exponent_bits_per_sample = get_exponent_bits(color_type);
514
  if (has_alpha) {
515
    ppf->info.alpha_bits = ppf->info.bits_per_sample;
516
    ppf->info.alpha_exponent_bits = ppf->info.exponent_bits_per_sample;
517
    ppf->info.alpha_premultiplied = JXL_TRUE;
518
  }
519
  ppf->info.intensity_target = intensity_target;
520
  return true;
521
}
522
523
}  // namespace extras
524
}  // namespace jxl
525
526
#endif  // JPEGXL_ENABLE_EXR