/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 |