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