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