Coverage Report

Created: 2024-05-21 06:41

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