Coverage Report

Created: 2025-06-16 07:00

/src/libjxl/lib/jxl/jpeg/enc_jpeg_data.cc
Line
Count
Source (jump to first uncovered line)
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/jxl/jpeg/enc_jpeg_data.h"
7
8
#include <brotli/encode.h>
9
#include <jxl/cms.h>
10
#include <jxl/memory_manager.h>
11
#include <jxl/types.h>
12
13
#include <algorithm>
14
#include <cstddef>
15
#include <cstdint>
16
#include <cstring>
17
#include <memory>
18
#include <utility>
19
#include <vector>
20
21
#include "lib/jxl/base/common.h"
22
#include "lib/jxl/base/sanitizers.h"
23
#include "lib/jxl/base/span.h"
24
#include "lib/jxl/base/status.h"
25
#include "lib/jxl/color_encoding_internal.h"
26
#include "lib/jxl/enc_aux_out.h"
27
#include "lib/jxl/enc_bit_writer.h"
28
#include "lib/jxl/enc_params.h"
29
#include "lib/jxl/fields.h"
30
#include "lib/jxl/frame_header.h"
31
#include "lib/jxl/jpeg/enc_jpeg_data_reader.h"
32
#include "lib/jxl/jpeg/jpeg_data.h"
33
#include "lib/jxl/padded_bytes.h"
34
35
namespace jxl {
36
namespace jpeg {
37
38
namespace {
39
40
// TODO(eustas): move to jpeg_data, to use from codec_jpg as well.
41
// See if there is a canonically chunked ICC profile and mark corresponding
42
// app-tags with AppMarkerType::kICC.
43
0
Status DetectIccProfile(JPEGData& jpeg_data) {
44
0
  JXL_ENSURE(jpeg_data.app_data.size() == jpeg_data.app_marker_type.size());
45
0
  size_t num_icc = 0;
46
0
  size_t num_icc_jpeg = 0;
47
0
  for (size_t i = 0; i < jpeg_data.app_data.size(); i++) {
48
0
    const auto& app = jpeg_data.app_data[i];
49
0
    size_t pos = 0;
50
0
    if (app[pos++] != 0xE2) continue;
51
    // At least APPn + size; otherwise it should be intermarker-data.
52
0
    JXL_ENSURE(app.size() >= 3);
53
0
    size_t tag_length = (app[pos] << 8) + app[pos + 1];
54
0
    pos += 2;
55
0
    JXL_ENSURE(app.size() == tag_length + 1);
56
    // Empty payload is 2 bytes for tag length itself + signature
57
0
    if (tag_length < 2 + sizeof kIccProfileTag) continue;
58
59
0
    if (memcmp(&app[pos], kIccProfileTag, sizeof kIccProfileTag) != 0) continue;
60
0
    pos += sizeof kIccProfileTag;
61
0
    uint8_t chunk_id = app[pos++];
62
0
    uint8_t num_chunks = app[pos++];
63
0
    if (chunk_id != num_icc + 1) continue;
64
0
    if (num_icc_jpeg == 0) num_icc_jpeg = num_chunks;
65
0
    if (num_icc_jpeg != num_chunks) continue;
66
0
    num_icc++;
67
0
    jpeg_data.app_marker_type[i] = AppMarkerType::kICC;
68
0
  }
69
0
  if (num_icc != num_icc_jpeg) {
70
0
    return JXL_FAILURE("Invalid ICC chunks");
71
0
  }
72
0
  return true;
73
0
}
74
75
0
bool GetMarkerPayload(const uint8_t* data, size_t size, Bytes* payload) {
76
0
  if (size < 3) {
77
0
    return false;
78
0
  }
79
0
  size_t hi = data[1];
80
0
  size_t lo = data[2];
81
0
  size_t internal_size = (hi << 8u) | lo;
82
  // Second byte of marker is not counted towards size.
83
0
  if (internal_size != size - 1) {
84
0
    return false;
85
0
  }
86
  // cut second marker byte and "length" from payload.
87
0
  *payload = Bytes(data, size);
88
0
  if (!payload->remove_prefix(3)) return false;
89
0
  return true;
90
0
}
91
92
0
Status DetectBlobs(jpeg::JPEGData& jpeg_data) {
93
0
  JXL_ENSURE(jpeg_data.app_data.size() == jpeg_data.app_marker_type.size());
94
0
  bool have_exif = false;
95
0
  bool have_xmp = false;
96
0
  for (size_t i = 0; i < jpeg_data.app_data.size(); i++) {
97
0
    auto& marker = jpeg_data.app_data[i];
98
0
    if (marker.empty() || marker[0] != kApp1) {
99
0
      continue;
100
0
    }
101
0
    Bytes payload;
102
0
    if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) {
103
      // Something is wrong with this marker; does not care.
104
0
      continue;
105
0
    }
106
0
    if (!have_exif && payload.size() > sizeof kExifTag &&
107
0
        !memcmp(payload.data(), kExifTag, sizeof kExifTag)) {
108
0
      jpeg_data.app_marker_type[i] = AppMarkerType::kExif;
109
0
      have_exif = true;
110
0
    }
111
0
    if (!have_xmp && payload.size() >= sizeof kXMPTag &&
112
0
        !memcmp(payload.data(), kXMPTag, sizeof kXMPTag)) {
113
0
      jpeg_data.app_marker_type[i] = AppMarkerType::kXMP;
114
0
      have_xmp = true;
115
0
    }
116
0
  }
117
0
  return true;
118
0
}
119
120
Status ParseChunkedMarker(const jpeg::JPEGData& src, uint8_t marker_type,
121
                          const Bytes& tag, IccBytes* output,
122
0
                          bool allow_permutations = false) {
123
0
  output->clear();
124
125
0
  std::vector<Bytes> chunks;
126
0
  std::vector<bool> presence;
127
0
  size_t expected_number_of_parts = 0;
128
0
  bool is_first_chunk = true;
129
0
  size_t ordinal = 0;
130
0
  for (const auto& marker : src.app_data) {
131
0
    if (marker.empty() || marker[0] != marker_type) {
132
0
      continue;
133
0
    }
134
0
    Bytes payload;
135
0
    if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) {
136
      // Something is wrong with this marker; does not care.
137
0
      continue;
138
0
    }
139
0
    if ((payload.size() < tag.size()) ||
140
0
        memcmp(payload.data(), tag.data(), tag.size()) != 0) {
141
0
      continue;
142
0
    }
143
0
    JXL_RETURN_IF_ERROR(payload.remove_prefix(tag.size()));
144
0
    if (payload.size() < 2) {
145
0
      return JXL_FAILURE("Chunk is too small.");
146
0
    }
147
0
    uint8_t index = payload[0];
148
0
    uint8_t total = payload[1];
149
0
    ordinal++;
150
0
    if (!allow_permutations) {
151
0
      if (index != ordinal) return JXL_FAILURE("Invalid chunk order.");
152
0
    }
153
154
0
    JXL_RETURN_IF_ERROR(payload.remove_prefix(2));
155
156
0
    JXL_RETURN_IF_ERROR(total != 0);
157
0
    if (is_first_chunk) {
158
0
      is_first_chunk = false;
159
0
      expected_number_of_parts = total;
160
      // 1-based indices; 0-th element is added for convenience.
161
0
      chunks.resize(total + 1);
162
0
      presence.resize(total + 1);
163
0
    } else {
164
0
      JXL_RETURN_IF_ERROR(expected_number_of_parts == total);
165
0
    }
166
167
0
    if (index == 0 || index > total) {
168
0
      return JXL_FAILURE("Invalid chunk index.");
169
0
    }
170
171
0
    if (presence[index]) {
172
0
      return JXL_FAILURE("Duplicate chunk.");
173
0
    }
174
0
    presence[index] = true;
175
0
    chunks[index] = payload;
176
0
  }
177
178
0
  for (size_t i = 0; i < expected_number_of_parts; ++i) {
179
    // 0-th element is not used.
180
0
    size_t index = i + 1;
181
0
    if (!presence[index]) {
182
0
      return JXL_FAILURE("Missing chunk.");
183
0
    }
184
0
    chunks[index].AppendTo(*output);
185
0
  }
186
187
0
  return true;
188
0
}
189
190
0
inline bool IsJPG(const Bytes bytes) {
191
0
  return bytes.size() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xD8;
192
0
}
193
194
}  // namespace
195
196
Status SetColorEncodingFromJpegData(const jpeg::JPEGData& jpg,
197
0
                                    ColorEncoding* color_encoding) {
198
0
  IccBytes icc_profile;
199
0
  if (!ParseChunkedMarker(jpg, kApp2, Bytes(kIccProfileTag), &icc_profile)) {
200
0
    JXL_WARNING("ReJPEG: corrupted ICC profile\n");
201
0
    icc_profile.clear();
202
0
  }
203
204
0
  if (icc_profile.empty()) {
205
0
    bool is_gray = (jpg.components.size() == 1);
206
0
    *color_encoding = ColorEncoding::SRGB(is_gray);
207
0
  } else {
208
0
    JXL_RETURN_IF_ERROR(
209
0
        color_encoding->SetICC(std::move(icc_profile), JxlGetDefaultCms()));
210
0
  }
211
0
  return true;
212
0
}
213
214
Status SetChromaSubsamplingFromJpegData(const JPEGData& jpg,
215
0
                                        YCbCrChromaSubsampling* cs) {
216
0
  size_t nbcomp = jpg.components.size();
217
0
  if (nbcomp != 1 && nbcomp != 3) {
218
0
    return JXL_FAILURE("Cannot recompress JPEGs with neither 1 nor 3 channels");
219
0
  }
220
0
  if (nbcomp == 3) {
221
0
    uint8_t hsample[3];
222
0
    uint8_t vsample[3];
223
0
    for (size_t i = 0; i < nbcomp; i++) {
224
0
      hsample[i] = jpg.components[i].h_samp_factor;
225
0
      vsample[i] = jpg.components[i].v_samp_factor;
226
0
    }
227
0
    JXL_RETURN_IF_ERROR(cs->Set(hsample, vsample));
228
0
  } else if (nbcomp == 1) {
229
0
    uint8_t hsample[3];
230
0
    uint8_t vsample[3];
231
0
    for (size_t i = 0; i < 3; i++) {
232
0
      hsample[i] = jpg.components[0].h_samp_factor;
233
0
      vsample[i] = jpg.components[0].v_samp_factor;
234
0
    }
235
0
    JXL_RETURN_IF_ERROR(cs->Set(hsample, vsample));
236
0
  }
237
0
  return true;
238
0
}
239
240
Status SetColorTransformFromJpegData(const JPEGData& jpg,
241
0
                                     ColorTransform* color_transform) {
242
0
  size_t nbcomp = jpg.components.size();
243
0
  if (nbcomp != 1 && nbcomp != 3) {
244
0
    return JXL_FAILURE("Cannot recompress JPEGs with neither 1 nor 3 channels");
245
0
  }
246
0
  bool is_rgb = false;
247
0
  {
248
0
    const auto& markers = jpg.marker_order;
249
    // If there is a JFIF marker, this is YCbCr. Otherwise...
250
0
    if (std::find(markers.begin(), markers.end(), 0xE0) == markers.end()) {
251
      // Try to find an 'Adobe' marker.
252
0
      size_t app_markers = 0;
253
0
      size_t i = 0;
254
0
      for (; i < markers.size(); i++) {
255
        // This is an APP marker.
256
0
        if ((markers[i] & 0xF0) == 0xE0) {
257
0
          JXL_ENSURE(app_markers < jpg.app_data.size());
258
          // APP14 marker
259
0
          if (markers[i] == 0xEE) {
260
0
            const auto& data = jpg.app_data[app_markers];
261
0
            if (data.size() == 15 && data[3] == 'A' && data[4] == 'd' &&
262
0
                data[5] == 'o' && data[6] == 'b' && data[7] == 'e') {
263
              // 'Adobe' marker.
264
0
              is_rgb = data[14] == 0;
265
0
              break;
266
0
            }
267
0
          }
268
0
          app_markers++;
269
0
        }
270
0
      }
271
272
0
      if (i == markers.size()) {
273
        // No 'Adobe' marker, guess from component IDs.
274
0
        is_rgb = nbcomp == 3 && jpg.components[0].id == 'R' &&
275
0
                 jpg.components[1].id == 'G' && jpg.components[2].id == 'B';
276
0
      }
277
0
    }
278
0
  }
279
0
  *color_transform =
280
0
      (!is_rgb || nbcomp == 1) ? ColorTransform::kYCbCr : ColorTransform::kNone;
281
0
  return true;
282
0
}
283
284
Status EncodeJPEGData(JxlMemoryManager* memory_manager, JPEGData& jpeg_data,
285
                      std::vector<uint8_t>* bytes,
286
0
                      const CompressParams& cparams) {
287
0
  bytes->clear();
288
0
  jpeg_data.app_marker_type.resize(jpeg_data.app_data.size(),
289
0
                                   AppMarkerType::kUnknown);
290
0
  JXL_RETURN_IF_ERROR(DetectIccProfile(jpeg_data));
291
0
  JXL_RETURN_IF_ERROR(DetectBlobs(jpeg_data));
292
293
0
  size_t total_data = 0;
294
0
  for (size_t i = 0; i < jpeg_data.app_data.size(); i++) {
295
0
    if (jpeg_data.app_marker_type[i] != AppMarkerType::kUnknown) {
296
0
      continue;
297
0
    }
298
0
    total_data += jpeg_data.app_data[i].size();
299
0
  }
300
0
  for (const auto& data : jpeg_data.com_data) {
301
0
    total_data += data.size();
302
0
  }
303
0
  for (const auto& data : jpeg_data.inter_marker_data) {
304
0
    total_data += data.size();
305
0
  }
306
0
  total_data += jpeg_data.tail_data.size();
307
0
  size_t brotli_capacity = BrotliEncoderMaxCompressedSize(total_data);
308
309
0
  BitWriter writer{memory_manager};
310
0
  JXL_RETURN_IF_ERROR(
311
0
      Bundle::Write(jpeg_data, &writer, LayerType::Header, nullptr));
312
0
  writer.ZeroPadToByte();
313
0
  {
314
0
    PaddedBytes serialized_jpeg_data = std::move(writer).TakeBytes();
315
0
    bytes->reserve(serialized_jpeg_data.size() + brotli_capacity);
316
0
    Bytes(serialized_jpeg_data).AppendTo(*bytes);
317
0
  }
318
319
0
  BrotliEncoderState* brotli_enc =
320
0
      BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
321
0
  int effort = cparams.brotli_effort;
322
0
  if (effort < 0) effort = 11 - static_cast<int>(cparams.speed_tier);
323
0
  BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_QUALITY, effort);
324
0
  size_t initial_size = bytes->size();
325
0
  BrotliEncoderSetParameter(brotli_enc, BROTLI_PARAM_SIZE_HINT, total_data);
326
0
  bytes->resize(initial_size + brotli_capacity);
327
0
  size_t enc_size = 0;
328
0
  auto br_append = [&](const std::vector<uint8_t>& data, bool last) -> Status {
329
0
    size_t available_in = data.size();
330
0
    const uint8_t* in = data.data();
331
0
    uint8_t* out = &(*bytes)[initial_size + enc_size];
332
0
    do {
333
0
      uint8_t* out_before = out;
334
0
      msan::MemoryIsInitialized(in, available_in);
335
0
      JXL_ENSURE(BrotliEncoderCompressStream(
336
0
          brotli_enc, last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS,
337
0
          &available_in, &in, &brotli_capacity, &out, &enc_size));
338
0
      msan::UnpoisonMemory(out_before, out - out_before);
339
0
    } while (FROM_JXL_BOOL(BrotliEncoderHasMoreOutput(brotli_enc)) ||
340
0
             available_in > 0);
341
0
    return true;
342
0
  };
343
344
0
  for (size_t i = 0; i < jpeg_data.app_data.size(); i++) {
345
0
    if (jpeg_data.app_marker_type[i] != AppMarkerType::kUnknown) {
346
0
      continue;
347
0
    }
348
0
    JXL_RETURN_IF_ERROR(br_append(jpeg_data.app_data[i], /*last=*/false));
349
0
  }
350
0
  for (const auto& data : jpeg_data.com_data) {
351
0
    JXL_RETURN_IF_ERROR(br_append(data, /*last=*/false));
352
0
  }
353
0
  for (const auto& data : jpeg_data.inter_marker_data) {
354
0
    JXL_RETURN_IF_ERROR(br_append(data, /*last=*/false));
355
0
  }
356
0
  JXL_RETURN_IF_ERROR(br_append(jpeg_data.tail_data, /*last=*/true));
357
0
  BrotliEncoderDestroyInstance(brotli_enc);
358
0
  bytes->resize(initial_size + enc_size);
359
0
  return true;
360
0
}
361
362
StatusOr<std::unique_ptr<JPEGData>> ParseJPG(JxlMemoryManager* memory_manager,
363
0
                                             const Bytes bytes) {
364
0
  if (!IsJPG(bytes)) return JXL_FAILURE("Not JPEG");
365
0
  auto jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>();
366
0
  if (!jpeg::ReadJpeg(bytes.data(), bytes.size(), jpeg::JpegReadMode::kReadAll,
367
0
                      jpeg_data.get())) {
368
0
    return JXL_FAILURE("Error reading JPEG");
369
0
  }
370
0
  return jpeg_data;
371
0
}
372
373
0
Status SetBlobsFromJpegData(const jpeg::JPEGData& jpeg_data, Blobs* blobs) {
374
0
  for (const auto& marker : jpeg_data.app_data) {
375
0
    if (marker.empty() || marker[0] != kApp1) {
376
0
      continue;
377
0
    }
378
0
    Bytes payload;
379
0
    if (!GetMarkerPayload(marker.data(), marker.size(), &payload)) {
380
      // Something is wrong with this marker; does not care.
381
0
      continue;
382
0
    }
383
0
    if (payload.size() >= sizeof kExifTag &&
384
0
        !memcmp(payload.data(), kExifTag, sizeof kExifTag)) {
385
0
      if (blobs->exif.empty()) {
386
0
        blobs->exif.resize(payload.size() - sizeof kExifTag);
387
0
        memcpy(blobs->exif.data(), payload.data() + sizeof kExifTag,
388
0
               payload.size() - sizeof kExifTag);
389
0
      } else {
390
0
        JXL_WARNING(
391
0
            "ReJPEG: multiple Exif blobs, storing only first one in the JPEG "
392
0
            "XL container\n");
393
0
      }
394
0
    }
395
0
    if (payload.size() >= sizeof kXMPTag &&
396
0
        !memcmp(payload.data(), kXMPTag, sizeof kXMPTag)) {
397
0
      if (blobs->xmp.empty()) {
398
0
        blobs->xmp.resize(payload.size() - sizeof kXMPTag);
399
0
        memcpy(blobs->xmp.data(), payload.data() + sizeof kXMPTag,
400
0
               payload.size() - sizeof kXMPTag);
401
0
      } else {
402
0
        JXL_WARNING(
403
0
            "ReJPEG: multiple XMP blobs, storing only first one in the JPEG "
404
0
            "XL container\n");
405
0
      }
406
0
    }
407
0
  }
408
0
  return true;
409
0
}
410
411
}  // namespace jpeg
412
}  // namespace jxl