xref: /aosp_15_r20/external/tensorflow/tensorflow/core/lib/jpeg/jpeg_mem_unittest.cc (revision b6fb3261f9314811a0f4371741dbb8839866f948)
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