Coverage Report

Created: 2025-11-16 07:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/extras/enc/jxl.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/jxl.h"
7
8
#include <jxl/codestream_header.h>
9
#include <jxl/encode.h>
10
#include <jxl/encode_cxx.h>
11
#include <jxl/types.h>
12
13
#include <algorithm>
14
#include <cstddef>
15
#include <cstdint>
16
#include <cstdio>
17
#include <cstring>
18
#include <vector>
19
20
#include "lib/extras/packed_image.h"
21
#include "lib/jxl/base/exif.h"
22
23
namespace jxl {
24
namespace extras {
25
26
JxlEncoderStatus SetOption(const JXLOption& opt,
27
0
                           JxlEncoderFrameSettings* settings) {
28
0
  return opt.is_float
29
0
             ? JxlEncoderFrameSettingsSetFloatOption(settings, opt.id, opt.fval)
30
0
             : JxlEncoderFrameSettingsSetOption(settings, opt.id, opt.ival);
31
0
}
32
33
bool SetFrameOptions(const std::vector<JXLOption>& options, size_t frame_index,
34
0
                     size_t* option_idx, JxlEncoderFrameSettings* settings) {
35
0
  while (*option_idx < options.size()) {
36
0
    const auto& opt = options[*option_idx];
37
0
    if (opt.frame_index > frame_index) {
38
0
      break;
39
0
    }
40
0
    if (JXL_ENC_SUCCESS != SetOption(opt, settings)) {
41
0
      fprintf(stderr, "Setting option id %d failed.\n", opt.id);
42
0
      return false;
43
0
    }
44
0
    (*option_idx)++;
45
0
  }
46
0
  return true;
47
0
}
48
49
bool SetupFrame(JxlEncoder* enc, JxlEncoderFrameSettings* settings,
50
                const JxlFrameHeader& frame_header,
51
                const JXLCompressParams& params, const PackedPixelFile& ppf,
52
                size_t frame_index, size_t num_alpha_channels,
53
0
                size_t num_interleaved_alpha, size_t& option_idx) {
54
0
  if (JXL_ENC_SUCCESS != JxlEncoderSetFrameHeader(settings, &frame_header)) {
55
0
    fprintf(stderr, "JxlEncoderSetFrameHeader() failed.\n");
56
0
    return false;
57
0
  }
58
0
  if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) {
59
0
    return false;
60
0
  }
61
0
  if (num_alpha_channels > 0) {
62
0
    JxlExtraChannelInfo extra_channel_info;
63
0
    JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &extra_channel_info);
64
0
    extra_channel_info.bits_per_sample = ppf.info.alpha_bits;
65
0
    extra_channel_info.exponent_bits_per_sample = ppf.info.alpha_exponent_bits;
66
0
    extra_channel_info.alpha_premultiplied = ppf.info.alpha_premultiplied;
67
0
    if (params.premultiply != -1) {
68
0
      if (params.premultiply != 0 && params.premultiply != 1) {
69
0
        fprintf(stderr, "premultiply must be one of: -1, 0, 1.\n");
70
0
        return false;
71
0
      }
72
0
      extra_channel_info.alpha_premultiplied = params.premultiply;
73
0
    }
74
0
    if (JXL_ENC_SUCCESS !=
75
0
        JxlEncoderSetExtraChannelInfo(enc, 0, &extra_channel_info)) {
76
0
      fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
77
0
      return false;
78
0
    }
79
    // We take the extra channel blend info frame_info, but don't do
80
    // clamping.
81
0
    JxlBlendInfo extra_channel_blend_info = frame_header.layer_info.blend_info;
82
0
    extra_channel_blend_info.clamp = JXL_FALSE;
83
0
    JxlEncoderSetExtraChannelBlendInfo(settings, 0, &extra_channel_blend_info);
84
0
  }
85
  // Add extra channel info for the rest of the extra channels.
86
0
  for (size_t i = 0; i < ppf.info.num_extra_channels; ++i) {
87
0
    if (i < ppf.extra_channels_info.size()) {
88
0
      const auto& ec_info = ppf.extra_channels_info[i].ec_info;
89
0
      if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelInfo(
90
0
                                 enc, num_interleaved_alpha + i, &ec_info)) {
91
0
        fprintf(stderr, "JxlEncoderSetExtraChannelInfo() failed.\n");
92
0
        return false;
93
0
      }
94
0
      const auto& ec_name = ppf.extra_channels_info[i].name;
95
0
      if (!ec_name.empty()) {
96
0
        if (JXL_ENC_SUCCESS !=
97
0
            JxlEncoderSetExtraChannelName(enc, num_interleaved_alpha + i,
98
0
                                          ec_name.c_str(), ec_name.size())) {
99
0
          fprintf(stderr, "JxlEncoderSetExtraChannelName() failed.\n");
100
0
          return false;
101
0
        }
102
0
      }
103
0
    }
104
0
  }
105
0
  return true;
106
0
}
107
108
0
bool ReadCompressedOutput(JxlEncoder* enc, std::vector<uint8_t>* compressed) {
109
0
  compressed->clear();
110
0
  compressed->resize(4096);
111
0
  uint8_t* next_out = compressed->data();
112
0
  size_t avail_out = compressed->size() - (next_out - compressed->data());
113
0
  JxlEncoderStatus result = JXL_ENC_NEED_MORE_OUTPUT;
114
0
  while (result == JXL_ENC_NEED_MORE_OUTPUT) {
115
0
    result = JxlEncoderProcessOutput(enc, &next_out, &avail_out);
116
0
    if (result == JXL_ENC_NEED_MORE_OUTPUT) {
117
0
      size_t offset = next_out - compressed->data();
118
0
      compressed->resize(compressed->size() * 2);
119
0
      next_out = compressed->data() + offset;
120
0
      avail_out = compressed->size() - offset;
121
0
    }
122
0
  }
123
0
  compressed->resize(next_out - compressed->data());
124
0
  if (result != JXL_ENC_SUCCESS) {
125
0
    fprintf(stderr, "JxlEncoderProcessOutput failed.\n");
126
0
    return false;
127
0
  }
128
0
  return true;
129
0
}
130
131
bool EncodeImageJXL(const JXLCompressParams& params, const PackedPixelFile& ppf,
132
                    const std::vector<uint8_t>* jpeg_bytes,
133
0
                    std::vector<uint8_t>* compressed) {
134
0
  auto encoder = JxlEncoderMake(params.memory_manager);
135
0
  JxlEncoder* enc = encoder.get();
136
137
0
  if (params.allow_expert_options) {
138
0
    JxlEncoderAllowExpertOptions(enc);
139
0
  }
140
141
0
  if (params.runner_opaque != nullptr &&
142
0
      JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc, params.runner,
143
0
                                                     params.runner_opaque)) {
144
0
    fprintf(stderr, "JxlEncoderSetParallelRunner failed\n");
145
0
    return false;
146
0
  }
147
148
0
  if (params.HasOutputProcessor() &&
149
0
      JXL_ENC_SUCCESS !=
150
0
          JxlEncoderSetOutputProcessor(enc, params.output_processor)) {
151
0
    fprintf(stderr, "JxlEncoderSetOutputProcessor failed\n");
152
0
    return false;
153
0
  }
154
155
0
  auto* settings = JxlEncoderFrameSettingsCreate(enc, nullptr);
156
0
  if (!settings) {
157
0
    fprintf(stderr, "JxlEncoderFrameSettingsCreate failed\n");
158
0
    return false;
159
0
  }
160
0
  size_t option_idx = 0;
161
0
  if (!SetFrameOptions(params.options, 0, &option_idx, settings)) {
162
0
    return false;
163
0
  }
164
0
  if (JXL_ENC_SUCCESS !=
165
0
      JxlEncoderSetFrameDistance(settings, params.distance)) {
166
0
    fprintf(stderr, "Setting frame distance failed.\n");
167
0
    return false;
168
0
  }
169
0
  if (params.debug_image) {
170
0
    JxlEncoderSetDebugImageCallback(settings, params.debug_image,
171
0
                                    params.debug_image_opaque);
172
0
  }
173
0
  if (params.stats) {
174
0
    JxlEncoderCollectStats(settings, params.stats);
175
0
  }
176
177
0
  bool has_jpeg_bytes = (jpeg_bytes != nullptr);
178
0
  bool use_boxes = !ppf.metadata.exif.empty() || !ppf.metadata.xmp.empty() ||
179
0
                   !ppf.metadata.jhgm.empty() || !ppf.metadata.jumbf.empty() ||
180
0
                   !ppf.metadata.iptc.empty();
181
0
  bool use_container = params.use_container || use_boxes ||
182
0
                       (has_jpeg_bytes && params.jpeg_store_metadata);
183
184
0
  if (JXL_ENC_SUCCESS !=
185
0
      JxlEncoderUseContainer(enc, static_cast<int>(use_container))) {
186
0
    fprintf(stderr, "JxlEncoderUseContainer failed.\n");
187
0
    return false;
188
0
  }
189
190
0
  if (has_jpeg_bytes) {
191
0
    if (params.jpeg_store_metadata &&
192
0
        JXL_ENC_SUCCESS != JxlEncoderStoreJPEGMetadata(enc, JXL_TRUE)) {
193
0
      fprintf(stderr, "Storing JPEG metadata failed.\n");
194
0
      return false;
195
0
    }
196
0
    if (params.jpeg_store_metadata && params.jpeg_strip_exif) {
197
0
      fprintf(stderr,
198
0
              "Cannot store metadata and strip exif at the same time.\n");
199
0
      return false;
200
0
    }
201
0
    if (params.jpeg_store_metadata && params.jpeg_strip_xmp) {
202
0
      fprintf(stderr,
203
0
              "Cannot store metadata and strip xmp at the same time.\n");
204
0
      return false;
205
0
    }
206
0
    if (!params.jpeg_store_metadata && params.jpeg_strip_exif) {
207
0
      JxlEncoderFrameSettingsSetOption(settings,
208
0
                                       JXL_ENC_FRAME_SETTING_JPEG_KEEP_EXIF, 0);
209
0
    }
210
0
    if (!params.jpeg_store_metadata && params.jpeg_strip_xmp) {
211
0
      JxlEncoderFrameSettingsSetOption(settings,
212
0
                                       JXL_ENC_FRAME_SETTING_JPEG_KEEP_XMP, 0);
213
0
    }
214
0
    if (params.jpeg_strip_jumbf) {
215
0
      JxlEncoderFrameSettingsSetOption(
216
0
          settings, JXL_ENC_FRAME_SETTING_JPEG_KEEP_JUMBF, 0);
217
0
    }
218
0
    if (JXL_ENC_SUCCESS != JxlEncoderAddJPEGFrame(settings, jpeg_bytes->data(),
219
0
                                                  jpeg_bytes->size())) {
220
0
      JxlEncoderError error = JxlEncoderGetError(enc);
221
0
      if (error == JXL_ENC_ERR_BAD_INPUT) {
222
0
        fprintf(stderr,
223
0
                "Error while decoding the JPEG image. It may be corrupt (e.g. "
224
0
                "truncated) or of an unsupported type (e.g. CMYK).\n");
225
0
      } else if (error == JXL_ENC_ERR_JBRD) {
226
0
        fprintf(stderr,
227
0
                "JPEG bitstream reconstruction data could not be created. "
228
0
                "Possibly there is too much tail data.\n"
229
0
                "Try using --allow_jpeg_reconstruction 0, to losslessly "
230
0
                "recompress the JPEG image data without bitstream "
231
0
                "reconstruction data.\n");
232
0
      } else {
233
0
        fprintf(stderr, "JxlEncoderAddJPEGFrame() failed.\n");
234
0
      }
235
0
      return false;
236
0
    }
237
0
  } else {
238
0
    size_t num_alpha_channels = 0;  // Adjusted below.
239
0
    JxlBasicInfo basic_info = ppf.info;
240
0
    basic_info.xsize *= params.already_downsampled;
241
0
    basic_info.ysize *= params.already_downsampled;
242
0
    if (basic_info.alpha_bits > 0) num_alpha_channels = 1;
243
0
    if (params.intensity_target > 0) {
244
0
      basic_info.intensity_target = params.intensity_target;
245
0
    }
246
0
    basic_info.num_extra_channels =
247
0
        std::max<uint32_t>(num_alpha_channels, ppf.info.num_extra_channels);
248
0
    basic_info.num_color_channels = ppf.info.num_color_channels;
249
0
    const bool lossless = (params.distance == 0);
250
0
    auto non_perceptual_option = std::find_if(
251
0
        params.options.begin(), params.options.end(), [](JXLOption option) {
252
0
          return option.id ==
253
0
                 JXL_ENC_FRAME_SETTING_DISABLE_PERCEPTUAL_HEURISTICS;
254
0
        });
255
0
    const bool non_perceptual = non_perceptual_option != params.options.end() &&
256
0
                                non_perceptual_option->ival == 1;
257
0
    basic_info.uses_original_profile = TO_JXL_BOOL(lossless || non_perceptual);
258
0
    if (params.override_bitdepth != 0) {
259
0
      basic_info.bits_per_sample = params.override_bitdepth;
260
0
      basic_info.exponent_bits_per_sample =
261
0
          params.override_bitdepth == 32 ? 8 : 0;
262
0
    }
263
0
    if (JXL_ENC_SUCCESS !=
264
0
        JxlEncoderSetCodestreamLevel(enc, params.codestream_level)) {
265
0
      fprintf(stderr, "Setting --codestream_level failed.\n");
266
0
      return false;
267
0
    }
268
0
    if (JXL_ENC_SUCCESS != JxlEncoderSetBasicInfo(enc, &basic_info)) {
269
0
      fprintf(stderr, "JxlEncoderSetBasicInfo() failed.\n");
270
0
      return false;
271
0
    }
272
0
    if (JXL_ENC_SUCCESS !=
273
0
        JxlEncoderSetUpsamplingMode(enc, params.already_downsampled,
274
0
                                    params.upsampling_mode)) {
275
0
      fprintf(stderr, "JxlEncoderSetUpsamplingMode() failed.\n");
276
0
      return false;
277
0
    }
278
0
    if (JXL_ENC_SUCCESS !=
279
0
        JxlEncoderSetFrameBitDepth(settings, &ppf.input_bitdepth)) {
280
0
      fprintf(stderr, "JxlEncoderSetFrameBitDepth() failed.\n");
281
0
      return false;
282
0
    }
283
0
    if (num_alpha_channels != 0 &&
284
0
        JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelDistance(
285
0
                               settings, 0, params.alpha_distance)) {
286
0
      fprintf(stderr, "Setting alpha distance failed.\n");
287
0
      return false;
288
0
    }
289
0
    if (lossless &&
290
0
        JXL_ENC_SUCCESS != JxlEncoderSetFrameLossless(settings, JXL_TRUE)) {
291
0
      fprintf(stderr, "JxlEncoderSetFrameLossless() failed.\n");
292
0
      return false;
293
0
    }
294
0
    if (ppf.primary_color_representation == PackedPixelFile::kIccIsPrimary) {
295
0
      if (JXL_ENC_SUCCESS !=
296
0
          JxlEncoderSetICCProfile(enc, ppf.icc.data(), ppf.icc.size())) {
297
0
        fprintf(stderr, "JxlEncoderSetICCProfile() failed.\n");
298
0
        return false;
299
0
      }
300
0
    } else {
301
0
      if (JXL_ENC_SUCCESS !=
302
0
          JxlEncoderSetColorEncoding(enc, &ppf.color_encoding)) {
303
0
        fprintf(stderr, "JxlEncoderSetColorEncoding() failed.\n");
304
0
        return false;
305
0
      }
306
0
    }
307
308
0
    if (use_boxes) {
309
0
      if (JXL_ENC_SUCCESS != JxlEncoderUseBoxes(enc)) {
310
0
        fprintf(stderr, "JxlEncoderUseBoxes() failed.\n");
311
0
        return false;
312
0
      }
313
      // Prepend 4 zero bytes to exif for tiff header offset
314
0
      std::vector<uint8_t> exif_with_offset;
315
0
      bool bigendian;
316
0
      if (IsExif(ppf.metadata.exif, &bigendian)) {
317
0
        exif_with_offset.resize(ppf.metadata.exif.size() + 4);
318
0
        memcpy(exif_with_offset.data() + 4, ppf.metadata.exif.data(),
319
0
               ppf.metadata.exif.size());
320
0
      }
321
0
      const struct BoxInfo {
322
0
        const char* type;
323
0
        const std::vector<uint8_t>& bytes;
324
0
      } boxes[] = {
325
0
          {"Exif", exif_with_offset},   {"xml ", ppf.metadata.xmp},
326
0
          {"jumb", ppf.metadata.jumbf}, {"xml ", ppf.metadata.iptc},
327
0
          {"jhgm", ppf.metadata.jhgm},
328
0
      };
329
0
      for (auto box : boxes) {
330
0
        if (!box.bytes.empty()) {
331
0
          if (JXL_ENC_SUCCESS !=
332
0
              JxlEncoderAddBox(enc, box.type, box.bytes.data(),
333
0
                               box.bytes.size(),
334
0
                               TO_JXL_BOOL(params.compress_boxes))) {
335
0
            fprintf(stderr, "JxlEncoderAddBox() failed (%s).\n", box.type);
336
0
            return false;
337
0
          }
338
0
        }
339
0
      }
340
0
      JxlEncoderCloseBoxes(enc);
341
0
    }
342
343
0
    for (size_t num_frame = 0; num_frame < ppf.frames.size(); ++num_frame) {
344
0
      const jxl::extras::PackedFrame& pframe = ppf.frames[num_frame];
345
0
      const jxl::extras::PackedImage& pimage = pframe.color;
346
0
      JxlPixelFormat ppixelformat = pimage.format;
347
0
      size_t num_interleaved_alpha =
348
0
          (ppixelformat.num_channels - ppf.info.num_color_channels);
349
0
      if (!SetupFrame(enc, settings, pframe.frame_info, params, ppf, num_frame,
350
0
                      num_alpha_channels, num_interleaved_alpha, option_idx)) {
351
0
        return false;
352
0
      }
353
0
      if (JXL_ENC_SUCCESS != JxlEncoderAddImageFrame(settings, &ppixelformat,
354
0
                                                     pimage.pixels(),
355
0
                                                     pimage.pixels_size)) {
356
0
        fprintf(stderr, "JxlEncoderAddImageFrame() failed.\n");
357
0
        return false;
358
0
      }
359
      // Only set extra channel buffer if it is provided non-interleaved.
360
0
      for (size_t i = 0; i < pframe.extra_channels.size(); ++i) {
361
0
        if (JXL_ENC_SUCCESS != JxlEncoderSetExtraChannelBuffer(
362
0
                                   settings, &pframe.extra_channels[i].format,
363
0
                                   pframe.extra_channels[i].pixels(),
364
0
                                   pframe.extra_channels[i].stride *
365
0
                                       pframe.extra_channels[i].ysize,
366
0
                                   num_interleaved_alpha + i)) {
367
0
          fprintf(stderr, "JxlEncoderSetExtraChannelBuffer() failed.\n");
368
0
          return false;
369
0
        }
370
0
      }
371
0
    }
372
0
    for (size_t fi = 0; fi < ppf.chunked_frames.size(); ++fi) {
373
0
      ChunkedPackedFrame& chunked_frame = ppf.chunked_frames[fi];
374
0
      size_t num_interleaved_alpha =
375
0
          (chunked_frame.format.num_channels - ppf.info.num_color_channels);
376
0
      if (!SetupFrame(enc, settings, chunked_frame.frame_info, params, ppf, fi,
377
0
                      num_alpha_channels, num_interleaved_alpha, option_idx)) {
378
0
        return false;
379
0
      }
380
0
      const bool last_frame = fi + 1 == ppf.chunked_frames.size();
381
0
      if (JXL_ENC_SUCCESS !=
382
0
          JxlEncoderAddChunkedFrame(settings, TO_JXL_BOOL(last_frame),
383
0
                                    chunked_frame.GetInputSource())) {
384
0
        fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n");
385
0
        return false;
386
0
      }
387
0
    }
388
0
  }
389
0
  JxlEncoderCloseInput(enc);
390
0
  if (params.HasOutputProcessor()) {
391
0
    if (JXL_ENC_SUCCESS != JxlEncoderFlushInput(enc)) {
392
0
      fprintf(stderr, "JxlEncoderAddChunkedFrame() failed.\n");
393
0
      return false;
394
0
    }
395
0
  } else if (!ReadCompressedOutput(enc, compressed)) {
396
0
    return false;
397
0
  }
398
0
  return true;
399
0
}
400
401
}  // namespace extras
402
}  // namespace jxl