1 /* Copyright 2015 The TensorFlow Authors. All Rights Reserved.
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 ==============================================================================*/
15
16 #include "tensorflow/core/lib/jpeg/jpeg_mem.h"
17
18 #include <setjmp.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22
23 #include <memory>
24
25 #include "absl/base/casts.h"
26 #include "tensorflow/core/lib/jpeg/jpeg_handle.h"
27 #include "tensorflow/core/platform/env.h"
28 #include "tensorflow/core/platform/logging.h"
29 #include "tensorflow/core/platform/test.h"
30 #include "tensorflow/core/platform/types.h"
31
32
33 namespace tensorflow {
34 namespace jpeg {
35 namespace {
36
37 const char kTestData[] = "tensorflow/core/lib/jpeg/testdata/";
38
ComputeSumAbsoluteDifference(const uint8 * a,const uint8 * b,int width,int height,int a_stride,int b_stride)39 int ComputeSumAbsoluteDifference(const uint8* a, const uint8* b, int width,
40 int height, int a_stride, int b_stride) {
41 int totalerr = 0;
42 for (int i = 0; i < height; i++) {
43 const uint8* const pa = a + i * a_stride;
44 const uint8* const pb = b + i * b_stride;
45 for (int j = 0; j < 3 * width; j++) {
46 totalerr += abs(static_cast<int>(pa[j]) - static_cast<int>(pb[j]));
47 }
48 }
49 return totalerr;
50 }
51
52 // Reads the contents of the file into output
ReadFileToStringOrDie(Env * env,const string & filename,string * output)53 void ReadFileToStringOrDie(Env* env, const string& filename, string* output) {
54 TF_CHECK_OK(ReadFileToString(env, filename, output));
55 }
56
TestJPEG(Env * env,const string & jpegfile)57 void TestJPEG(Env* env, const string& jpegfile) {
58 // Read the data from the jpeg file into memory
59 string jpeg;
60 ReadFileToStringOrDie(env, jpegfile, &jpeg);
61 const int fsize = jpeg.size();
62 const uint8* const temp = absl::bit_cast<const uint8*>(jpeg.data());
63
64 // Try partial decoding (half of the data)
65 int w, h, c;
66 std::unique_ptr<uint8[]> imgdata;
67
68 UncompressFlags flags;
69 flags.components = 3;
70
71 // Set min_acceptable_fraction to something insufficient
72 flags.min_acceptable_fraction = 0.8;
73 imgdata.reset(Uncompress(temp, fsize / 2, flags, &w, &h, &c, nullptr));
74 CHECK(imgdata == nullptr);
75
76 // Now, use a value that makes fsize/2 be enough for a black-filling
77 flags.min_acceptable_fraction = 0.01;
78 imgdata.reset(Uncompress(temp, fsize / 2, flags, &w, &h, &c, nullptr));
79 CHECK(imgdata != nullptr);
80
81 // Finally, uncompress the whole data
82 flags.min_acceptable_fraction = 1.0;
83 imgdata.reset(Uncompress(temp, fsize, flags, &w, &h, &c, nullptr));
84 CHECK(imgdata != nullptr);
85 }
86
TEST(JpegMemTest,Jpeg)87 TEST(JpegMemTest, Jpeg) {
88 Env* env = Env::Default();
89 const string data_path = kTestData;
90
91 // Name of a valid jpeg file on the disk
92 TestJPEG(env, data_path + "jpeg_merge_test1.jpg");
93
94 // Exercise CMYK machinery as well
95 TestJPEG(env, data_path + "jpeg_merge_test1_cmyk.jpg");
96 }
97
TestCropAndDecodeJpeg(Env * env,const string & jpegfile,const UncompressFlags & default_flags)98 void TestCropAndDecodeJpeg(Env* env, const string& jpegfile,
99 const UncompressFlags& default_flags) {
100 // Read the data from the jpeg file into memory
101 string jpeg;
102 ReadFileToStringOrDie(env, jpegfile, &jpeg);
103 const int fsize = jpeg.size();
104 const auto* temp = absl::bit_cast<const uint8*>(jpeg.data());
105
106 // Decode the whole image.
107 std::unique_ptr<uint8[]> imgdata1;
108 int w1, h1, c1;
109 {
110 UncompressFlags flags = default_flags;
111 if (flags.stride == 0) {
112 imgdata1.reset(Uncompress(temp, fsize, flags, &w1, &h1, &c1, nullptr));
113 } else {
114 // If stride is not zero, the default allocator would fail because it
115 // allocate w*h*c bytes, but the actual required bytes should be stride*h.
116 // Therefore, we provide a specialized allocator here.
117 uint8* buffer = nullptr;
118 imgdata1.reset(Uncompress(temp, fsize, flags, nullptr,
119 [&](int width, int height, int components) {
120 w1 = width;
121 h1 = height;
122 c1 = components;
123 buffer = new uint8[flags.stride * height];
124 return buffer;
125 }));
126 }
127 ASSERT_NE(imgdata1, nullptr);
128 }
129
130 auto check_crop_and_decode_func = [&](int crop_x, int crop_y, int crop_width,
131 int crop_height) {
132 std::unique_ptr<uint8[]> imgdata2;
133 int w, h, c;
134 UncompressFlags flags = default_flags;
135 flags.crop = true;
136 flags.crop_x = crop_x;
137 flags.crop_y = crop_y;
138 flags.crop_width = crop_width;
139 flags.crop_height = crop_height;
140 if (flags.stride == 0) {
141 imgdata2.reset(Uncompress(temp, fsize, flags, &w, &h, &c, nullptr));
142 } else {
143 uint8* buffer = nullptr;
144 imgdata2.reset(Uncompress(temp, fsize, flags, nullptr,
145 [&](int width, int height, int components) {
146 w = width;
147 h = height;
148 c = components;
149 buffer = new uint8[flags.stride * height];
150 return buffer;
151 }));
152 }
153 ASSERT_NE(imgdata2, nullptr);
154
155 ASSERT_EQ(w, crop_width);
156 ASSERT_EQ(h, crop_height);
157 ASSERT_EQ(c, c1);
158
159 const int stride1 = (flags.stride != 0) ? flags.stride : w1 * c;
160 const int stride2 = (flags.stride != 0) ? flags.stride : w * c;
161 for (int i = 0; i < crop_height; i++) {
162 const uint8* p1 = &imgdata1[(i + crop_y) * stride1 + crop_x * c];
163 const uint8* p2 = &imgdata2[i * stride2];
164
165 for (int j = 0; j < c * w; j++) {
166 ASSERT_EQ(p1[j], p2[j])
167 << "p1 != p2 in [" << i << "][" << j / 3 << "][" << j % 3 << "]";
168 }
169 }
170 };
171
172 // Check different crop windows.
173 check_crop_and_decode_func(0, 0, 5, 5);
174 check_crop_and_decode_func(0, 0, w1, 5);
175 check_crop_and_decode_func(0, 0, 5, h1);
176 check_crop_and_decode_func(0, 0, w1, h1);
177 check_crop_and_decode_func(w1 - 5, h1 - 6, 5, 6);
178 check_crop_and_decode_func(5, 6, 10, 15);
179 }
180
TEST(JpegMemTest,CropAndDecodeJpeg)181 TEST(JpegMemTest, CropAndDecodeJpeg) {
182 Env* env = Env::Default();
183 const string data_path = kTestData;
184 UncompressFlags flags;
185
186 // Test basic flags for jpeg and cmyk jpeg.
187 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
188 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1_cmyk.jpg", flags);
189 }
190
TEST(JpegMemTest,CropAndDecodeJpegWithRatio)191 TEST(JpegMemTest, CropAndDecodeJpegWithRatio) {
192 Env* env = Env::Default();
193 const string data_path = kTestData;
194 UncompressFlags flags;
195 for (int ratio : {1, 2, 4, 8}) {
196 flags.ratio = ratio;
197 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
198 }
199 }
200
TEST(JpegMemTest,CropAndDecodeJpegWithComponents)201 TEST(JpegMemTest, CropAndDecodeJpegWithComponents) {
202 Env* env = Env::Default();
203 const string data_path = kTestData;
204 UncompressFlags flags;
205 for (const int components : {0, 1, 3}) {
206 flags.components = components;
207 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
208 }
209 }
210
TEST(JpegMemTest,CropAndDecodeJpegWithUpScaling)211 TEST(JpegMemTest, CropAndDecodeJpegWithUpScaling) {
212 Env* env = Env::Default();
213 const string data_path = kTestData;
214 UncompressFlags flags;
215 flags.fancy_upscaling = true;
216 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
217 }
218
TEST(JpegMemTest,CropAndDecodeJpegWithStride)219 TEST(JpegMemTest, CropAndDecodeJpegWithStride) {
220 Env* env = Env::Default();
221 const string data_path = kTestData;
222
223 // Read the data from the jpeg file into memory
224 string jpeg;
225 ReadFileToStringOrDie(env, data_path + "jpeg_merge_test1.jpg", &jpeg);
226 const int fsize = jpeg.size();
227 const auto* temp = absl::bit_cast<const uint8*>(jpeg.data());
228
229 int w, h, c;
230 ASSERT_TRUE(GetImageInfo(temp, fsize, &w, &h, &c));
231
232 // stride must be either 0 or > w*c; otherwise, uncompress would fail.
233 UncompressFlags flags;
234 flags.stride = w * c;
235 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
236 flags.stride = w * c * 3;
237 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
238 flags.stride = w * c + 100;
239 TestCropAndDecodeJpeg(env, data_path + "jpeg_merge_test1.jpg", flags);
240 }
241
CheckInvalidCropWindowFailed(const uint8 * const temp,int fsize,int x,int y,int w,int h)242 void CheckInvalidCropWindowFailed(const uint8* const temp, int fsize, int x,
243 int y, int w, int h) {
244 std::unique_ptr<uint8[]> imgdata;
245 int ww, hh, cc;
246 UncompressFlags flags;
247 flags.components = 3;
248 flags.crop = true;
249 flags.crop_x = x;
250 flags.crop_y = y;
251 flags.crop_width = w;
252 flags.crop_height = h;
253 imgdata.reset(Uncompress(temp, fsize, flags, &ww, &hh, &cc, nullptr));
254 CHECK(imgdata == nullptr);
255 }
256
TEST(JpegMemTest,CropAndDecodeJpegWithInvalidCropWindow)257 TEST(JpegMemTest, CropAndDecodeJpegWithInvalidCropWindow) {
258 Env* env = Env::Default();
259 const string data_path = kTestData;
260
261 // Read the data from the jpeg file into memory
262 string jpeg;
263 ReadFileToStringOrDie(env, data_path + "jpeg_merge_test1.jpg", &jpeg);
264 const int fsize = jpeg.size();
265 const auto* temp = absl::bit_cast<const uint8*>(jpeg.data());
266
267 int w, h, c;
268 ASSERT_TRUE(GetImageInfo(temp, fsize, &w, &h, &c));
269
270 // Width and height for the crop window must be non zero.
271 CheckInvalidCropWindowFailed(temp, fsize, 11, 11, /*w=*/0, 11);
272 CheckInvalidCropWindowFailed(temp, fsize, 11, 11, 11, /*h=*/0);
273
274 // Crop window must be non negative.
275 CheckInvalidCropWindowFailed(temp, fsize, /*x=*/-1, 11, 11, 11);
276 CheckInvalidCropWindowFailed(temp, fsize, 11, /*y=*/-1, 11, 11);
277 CheckInvalidCropWindowFailed(temp, fsize, 11, 11, /*w=*/-1, 11);
278 CheckInvalidCropWindowFailed(temp, fsize, 11, 11, 11, /*h=*/-1);
279
280 // Invalid crop window width: x + crop_width = w + 1 > w
281 CheckInvalidCropWindowFailed(temp, fsize, /*x=*/w - 10, 11, 11, 11);
282 // Invalid crop window height: y + crop_height= h + 1 > h
283 CheckInvalidCropWindowFailed(temp, fsize, 11, /*y=*/h - 10, 11, 11);
284 }
285
TEST(JpegMemTest,Jpeg2)286 TEST(JpegMemTest, Jpeg2) {
287 // create known data, for size in_w x in_h
288 const int in_w = 256;
289 const int in_h = 256;
290 const int stride1 = 3 * in_w;
291 const std::unique_ptr<uint8[]> refdata1(new uint8[stride1 * in_h]);
292 for (int i = 0; i < in_h; i++) {
293 for (int j = 0; j < in_w; j++) {
294 const int offset = i * stride1 + 3 * j;
295 refdata1[offset + 0] = i;
296 refdata1[offset + 1] = j;
297 refdata1[offset + 2] = static_cast<uint8>((i + j) >> 1);
298 }
299 }
300
301 // duplicate with weird input stride
302 const int stride2 = 3 * 357;
303 const std::unique_ptr<uint8[]> refdata2(new uint8[stride2 * in_h]);
304 for (int i = 0; i < in_h; i++) {
305 memcpy(&refdata2[i * stride2], &refdata1[i * stride1], 3 * in_w);
306 }
307
308 // Test compression
309 string cpdata1, cpdata2;
310 {
311 const string kXMP = "XMP_TEST_123";
312
313 // Compress it to JPEG
314 CompressFlags flags;
315 flags.format = FORMAT_RGB;
316 flags.quality = 97;
317 flags.xmp_metadata = kXMP;
318 cpdata1 = Compress(refdata1.get(), in_w, in_h, flags);
319 flags.stride = stride2;
320 cpdata2 = Compress(refdata2.get(), in_w, in_h, flags);
321 // Different input stride shouldn't change the output
322 CHECK_EQ(cpdata1, cpdata2);
323
324 // Verify valid XMP.
325 CHECK_NE(string::npos, cpdata1.find(kXMP));
326
327 // Test the other API, where a storage string is supplied
328 tstring cptest;
329 flags.stride = 0;
330 Compress(refdata1.get(), in_w, in_h, flags, &cptest);
331 CHECK_EQ(cptest, cpdata1);
332 flags.stride = stride2;
333 Compress(refdata2.get(), in_w, in_h, flags, &cptest);
334 CHECK_EQ(cptest, cpdata2);
335 }
336
337 // Uncompress twice: once with 3 components and once with autodetect.
338 std::unique_ptr<uint8[]> imgdata1;
339 for (const int components : {0, 3}) {
340 // Uncompress it
341 UncompressFlags flags;
342 flags.components = components;
343 int w, h, c;
344 imgdata1.reset(Uncompress(cpdata1.c_str(), cpdata1.length(), flags, &w, &h,
345 &c, nullptr));
346
347 // Check obvious formatting stuff
348 CHECK_EQ(w, in_w);
349 CHECK_EQ(h, in_h);
350 CHECK_EQ(c, 3);
351 CHECK(imgdata1.get());
352
353 // Compare the two images
354 const int totalerr = ComputeSumAbsoluteDifference(
355 imgdata1.get(), refdata1.get(), in_w, in_h, stride1, stride1);
356 CHECK_LE(totalerr, 85000);
357 }
358
359 // check the second image too. Should be bitwise identical to the first.
360 // uncompress using a weird stride
361 {
362 UncompressFlags flags;
363 flags.stride = 3 * 411;
364 const std::unique_ptr<uint8[]> imgdata2(new uint8[flags.stride * in_h]);
365 CHECK(imgdata2.get() == Uncompress(cpdata2.c_str(), cpdata2.length(), flags,
366 nullptr /* nwarn */,
367 [=, &imgdata2](int w, int h, int c) {
368 CHECK_EQ(w, in_w);
369 CHECK_EQ(h, in_h);
370 CHECK_EQ(c, 3);
371 return imgdata2.get();
372 }));
373 const int totalerr = ComputeSumAbsoluteDifference(
374 imgdata1.get(), imgdata2.get(), in_w, in_h, stride1, flags.stride);
375 CHECK_EQ(totalerr, 0);
376 }
377
378 {
379 // Uncompress it with a faster, lossier algorithm.
380 UncompressFlags flags;
381 flags.components = 3;
382 flags.dct_method = JDCT_IFAST;
383 int w, h, c;
384 imgdata1.reset(Uncompress(cpdata1.c_str(), cpdata1.length(), flags, &w, &h,
385 &c, nullptr));
386
387 // Check obvious formatting stuff
388 CHECK_EQ(w, in_w);
389 CHECK_EQ(h, in_h);
390 CHECK_EQ(c, 3);
391 CHECK(imgdata1.get());
392
393 // Compare the two images
394 const int totalerr = ComputeSumAbsoluteDifference(
395 imgdata1.get(), refdata1.get(), in_w, in_h, stride1, stride1);
396 ASSERT_LE(totalerr, 200000);
397 }
398 }
399
400 // Takes JPEG data and reads its headers to determine whether or not the JPEG
401 // was chroma downsampled.
IsChromaDownsampled(const string & jpegdata)402 bool IsChromaDownsampled(const string& jpegdata) {
403 // Initialize libjpeg structures to have a memory source
404 // Modify the usual jpeg error manager to catch fatal errors.
405 struct jpeg_decompress_struct cinfo;
406 struct jpeg_error_mgr jerr;
407 jmp_buf jpeg_jmpbuf;
408 cinfo.err = jpeg_std_error(&jerr);
409 cinfo.client_data = &jpeg_jmpbuf;
410 jerr.error_exit = CatchError;
411 if (setjmp(jpeg_jmpbuf)) return false;
412
413 // set up, read header, set image parameters, save size
414 jpeg_create_decompress(&cinfo);
415 SetSrc(&cinfo, jpegdata.c_str(), jpegdata.size(), false);
416
417 jpeg_read_header(&cinfo, TRUE);
418 jpeg_start_decompress(&cinfo); // required to transfer image size to cinfo
419 const int components = cinfo.output_components;
420 if (components == 1) return false;
421
422 // Check validity
423 CHECK_EQ(3, components);
424 CHECK_EQ(cinfo.comp_info[1].h_samp_factor, cinfo.comp_info[2].h_samp_factor)
425 << "The h sampling factors should be the same.";
426 CHECK_EQ(cinfo.comp_info[1].v_samp_factor, cinfo.comp_info[2].v_samp_factor)
427 << "The v sampling factors should be the same.";
428 for (int i = 0; i < components; ++i) {
429 CHECK_GT(cinfo.comp_info[i].h_samp_factor, 0) << "Invalid sampling factor.";
430 CHECK_EQ(cinfo.comp_info[i].h_samp_factor, cinfo.comp_info[i].v_samp_factor)
431 << "The sampling factor should be the same in both directions.";
432 }
433
434 // We're downsampled if we use fewer samples for color than for brightness.
435 // Do this before deallocating cinfo.
436 const bool downsampled =
437 cinfo.comp_info[1].h_samp_factor < cinfo.comp_info[0].h_samp_factor;
438
439 jpeg_destroy_decompress(&cinfo);
440 return downsampled;
441 }
442
TEST(JpegMemTest,ChromaDownsampling)443 TEST(JpegMemTest, ChromaDownsampling) {
444 // Read the data from a test jpeg file into memory
445 const string jpegfile = string(kTestData) + "jpeg_merge_test1.jpg";
446 string jpeg;
447 ReadFileToStringOrDie(Env::Default(), jpegfile, &jpeg);
448
449 // Verify that compressing the JPEG with chroma downsampling works.
450 //
451 // First, uncompress the JPEG.
452 UncompressFlags unflags;
453 unflags.components = 3;
454 int w, h, c;
455 int64_t num_warnings;
456 std::unique_ptr<uint8[]> uncompressed(Uncompress(
457 jpeg.c_str(), jpeg.size(), unflags, &w, &h, &c, &num_warnings));
458 CHECK(uncompressed != nullptr);
459 CHECK_EQ(num_warnings, 0);
460
461 // Recompress the JPEG with and without chroma downsampling
462 for (const bool downsample : {false, true}) {
463 CompressFlags flags;
464 flags.format = FORMAT_RGB;
465 flags.quality = 85;
466 flags.chroma_downsampling = downsample;
467 tstring recompressed;
468 Compress(uncompressed.get(), w, h, flags, &recompressed);
469 CHECK(!recompressed.empty());
470 CHECK_EQ(IsChromaDownsampled(recompressed), downsample);
471 }
472 }
473
TestBadJPEG(Env * env,const string & bad_jpeg_file,int expected_width,int expected_height,const string & reference_RGB_file,const bool try_recover_truncated_jpeg)474 void TestBadJPEG(Env* env, const string& bad_jpeg_file, int expected_width,
475 int expected_height, const string& reference_RGB_file,
476 const bool try_recover_truncated_jpeg) {
477 string jpeg;
478 ReadFileToStringOrDie(env, bad_jpeg_file, &jpeg);
479
480 UncompressFlags flags;
481 flags.components = 3;
482 flags.try_recover_truncated_jpeg = try_recover_truncated_jpeg;
483
484 int width, height, components;
485 std::unique_ptr<uint8[]> imgdata;
486 imgdata.reset(Uncompress(jpeg.c_str(), jpeg.size(), flags, &width, &height,
487 &components, nullptr));
488 if (expected_width > 0) { // we expect the file to decode into 'something'
489 CHECK_EQ(width, expected_width);
490 CHECK_EQ(height, expected_height);
491 CHECK_EQ(components, 3);
492 CHECK(imgdata.get());
493 if (!reference_RGB_file.empty()) {
494 string ref;
495 ReadFileToStringOrDie(env, reference_RGB_file, &ref);
496 CHECK(!memcmp(ref.data(), imgdata.get(), ref.size()));
497 }
498 } else { // no decodable
499 CHECK(!imgdata.get()) << "file:" << bad_jpeg_file;
500 }
501 }
502
TEST(JpegMemTest,BadJpeg)503 TEST(JpegMemTest, BadJpeg) {
504 Env* env = Env::Default();
505 const string data_path = kTestData;
506
507 // Test corrupt file
508 TestBadJPEG(env, data_path + "bad_huffman.jpg", 1024, 768, "", false);
509 TestBadJPEG(env, data_path + "corrupt.jpg", 0 /*120*/, 90, "", false);
510
511 // Truncated files, undecodable because of missing lines:
512 TestBadJPEG(env, data_path + "corrupt34_2.jpg", 0, 3300, "", false);
513 TestBadJPEG(env, data_path + "corrupt34_3.jpg", 0, 3300, "", false);
514 TestBadJPEG(env, data_path + "corrupt34_4.jpg", 0, 3300, "", false);
515
516 // Try in 'recover' mode now:
517 TestBadJPEG(env, data_path + "corrupt34_2.jpg", 2544, 3300, "", true);
518 TestBadJPEG(env, data_path + "corrupt34_3.jpg", 2544, 3300, "", true);
519 TestBadJPEG(env, data_path + "corrupt34_4.jpg", 2544, 3300, "", true);
520 }
521
522 } // namespace
523 } // namespace jpeg
524 } // namespace tensorflow
525