diff options
Diffstat (limited to 'tensorflow/core/lib/jpeg')
-rw-r--r-- | tensorflow/core/lib/jpeg/jpeg_handle.cc | 162 | ||||
-rw-r--r-- | tensorflow/core/lib/jpeg/jpeg_handle.h | 51 | ||||
-rw-r--r-- | tensorflow/core/lib/jpeg/jpeg_mem.cc | 557 | ||||
-rw-r--r-- | tensorflow/core/lib/jpeg/jpeg_mem.h | 130 | ||||
-rw-r--r-- | tensorflow/core/lib/jpeg/jpeg_mem_unittest.cc | 304 | ||||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/bad_huffman.jpg | bin | 0 -> 15416 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/corrupt.jpg | bin | 0 -> 1552 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/corrupt34_2.jpg | bin | 0 -> 755 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/corrupt34_3.jpg | bin | 0 -> 5505 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/corrupt34_4.jpg | bin | 0 -> 5092 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1.jpg | bin | 0 -> 3771 bytes | |||
-rw-r--r-- | tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1_cmyk.jpg | bin | 0 -> 5324 bytes |
12 files changed, 1204 insertions, 0 deletions
diff --git a/tensorflow/core/lib/jpeg/jpeg_handle.cc b/tensorflow/core/lib/jpeg/jpeg_handle.cc new file mode 100644 index 0000000000..4521be0afb --- /dev/null +++ b/tensorflow/core/lib/jpeg/jpeg_handle.cc @@ -0,0 +1,162 @@ +// This file implements a memory destination for libjpeg +// The design is very similar to jdatadst.c in libjpeg +// These functions are not meant to be used directly, see jpeg_mem.h instead. +// We are filling out stubs required by jpeglib, those stubs are private to +// the implementation, we are just making available JPGMemSrc, JPGMemDest + +#include "tensorflow/core/lib/jpeg/jpeg_handle.h" + +#include <setjmp.h> +#include <stddef.h> + +#include "tensorflow/core/platform/logging.h" + +namespace tensorflow { +namespace jpeg { + +void CatchError(j_common_ptr cinfo) { + (*cinfo->err->output_message)(cinfo); + jmp_buf *jpeg_jmpbuf = reinterpret_cast<jmp_buf *>(cinfo->client_data); + jpeg_destroy(cinfo); + longjmp(*jpeg_jmpbuf, 1); +} + +// ***************************************************************************** +// ***************************************************************************** +// ***************************************************************************** +// Destination functions + +// ----------------------------------------------------------------------------- +void MemInitDestination(j_compress_ptr cinfo) { + MemDestMgr *dest = reinterpret_cast<MemDestMgr *>(cinfo->dest); + VLOG(1) << "Initializing buffer=" << dest->bufsize << " bytes"; + dest->pub.next_output_byte = dest->buffer; + dest->pub.free_in_buffer = dest->bufsize; + dest->datacount = 0; + if (dest->dest) { + dest->dest->clear(); + } +} + +// ----------------------------------------------------------------------------- +boolean MemEmptyOutputBuffer(j_compress_ptr cinfo) { + MemDestMgr *dest = reinterpret_cast<MemDestMgr *>(cinfo->dest); + VLOG(1) << "Writing " << dest->bufsize << " bytes"; + if (dest->dest) { + dest->dest->append(reinterpret_cast<char *>(dest->buffer), dest->bufsize); + } + dest->pub.next_output_byte = dest->buffer; + dest->pub.free_in_buffer = dest->bufsize; + return TRUE; +} + +// ----------------------------------------------------------------------------- +void MemTermDestination(j_compress_ptr cinfo) { + MemDestMgr *dest = reinterpret_cast<MemDestMgr *>(cinfo->dest); + VLOG(1) << "Writing " << dest->bufsize - dest->pub.free_in_buffer << " bytes"; + if (dest->dest) { + dest->dest->append(reinterpret_cast<char *>(dest->buffer), + dest->bufsize - dest->pub.free_in_buffer); + VLOG(1) << "Total size= " << dest->dest->size(); + } + dest->datacount = dest->bufsize - dest->pub.free_in_buffer; +} + +// ----------------------------------------------------------------------------- +void SetDest(j_compress_ptr cinfo, void *buffer, int bufsize) { + SetDest(cinfo, buffer, bufsize, NULL); +} + +// ----------------------------------------------------------------------------- +void SetDest(j_compress_ptr cinfo, void *buffer, int bufsize, + string *destination) { + MemDestMgr *dest; + if (cinfo->dest == NULL) { + cinfo->dest = reinterpret_cast<struct jpeg_destination_mgr *>( + (*cinfo->mem->alloc_small)(reinterpret_cast<j_common_ptr>(cinfo), + JPOOL_PERMANENT, sizeof(MemDestMgr))); + } + + dest = reinterpret_cast<MemDestMgr *>(cinfo->dest); + dest->bufsize = bufsize; + dest->buffer = static_cast<JOCTET *>(buffer); + dest->dest = destination; + dest->pub.init_destination = MemInitDestination; + dest->pub.empty_output_buffer = MemEmptyOutputBuffer; + dest->pub.term_destination = MemTermDestination; +} + +// ***************************************************************************** +// ***************************************************************************** +// ***************************************************************************** +// Source functions + +// ----------------------------------------------------------------------------- +void MemInitSource(j_decompress_ptr cinfo) { + MemSourceMgr *src = reinterpret_cast<MemSourceMgr *>(cinfo->src); + src->pub.next_input_byte = src->data; + src->pub.bytes_in_buffer = src->datasize; +} + +// ----------------------------------------------------------------------------- +// We emulate the same error-handling as fill_input_buffer() from jdatasrc.c, +// for coherency's sake. +boolean MemFillInputBuffer(j_decompress_ptr cinfo) { + static const JOCTET kEOIBuffer[2] = {0xff, JPEG_EOI}; + MemSourceMgr *src = reinterpret_cast<MemSourceMgr *>(cinfo->src); + if (src->pub.bytes_in_buffer == 0 && src->pub.next_input_byte == src->data) { + // empty file -> treated as an error. + ERREXIT(cinfo, JERR_INPUT_EMPTY); + return FALSE; + } else if (src->pub.bytes_in_buffer) { + // if there's still some data left, it's probably corrupted + return src->try_recover_truncated_jpeg ? TRUE : FALSE; + } else if (src->pub.next_input_byte != kEOIBuffer && + src->try_recover_truncated_jpeg) { + // In an attempt to recover truncated files, we insert a fake EOI + WARNMS(cinfo, JWRN_JPEG_EOF); + src->pub.next_input_byte = kEOIBuffer; + src->pub.bytes_in_buffer = 2; + return TRUE; + } else { + // We already inserted a fake EOI and it wasn't enough, so this time + // it's really an error. + ERREXIT(cinfo, JERR_FILE_READ); + return FALSE; + } +} + +// ----------------------------------------------------------------------------- +void MemTermSource(j_decompress_ptr cinfo) {} + +// ----------------------------------------------------------------------------- +void MemSkipInputData(j_decompress_ptr cinfo, long jump) { + MemSourceMgr *src = reinterpret_cast<MemSourceMgr *>(cinfo->src); + src->pub.bytes_in_buffer -= jump; + src->pub.next_input_byte += jump; +} + +// ----------------------------------------------------------------------------- +void SetSrc(j_decompress_ptr cinfo, const void *data, + unsigned long int datasize, bool try_recover_truncated_jpeg) { + MemSourceMgr *src; + + cinfo->src = reinterpret_cast<struct jpeg_source_mgr *>( + (*cinfo->mem->alloc_small)(reinterpret_cast<j_common_ptr>(cinfo), + JPOOL_PERMANENT, sizeof(MemSourceMgr))); + + src = reinterpret_cast<MemSourceMgr *>(cinfo->src); + src->pub.init_source = MemInitSource; + src->pub.fill_input_buffer = MemFillInputBuffer; + src->pub.skip_input_data = MemSkipInputData; + src->pub.resync_to_restart = jpeg_resync_to_restart; + src->pub.term_source = MemTermSource; + src->data = reinterpret_cast<const unsigned char *>(data); + src->datasize = datasize; + src->pub.bytes_in_buffer = 0; + src->pub.next_input_byte = NULL; + src->try_recover_truncated_jpeg = try_recover_truncated_jpeg; +} + +} // namespace jpeg +} // namespace tensorflow diff --git a/tensorflow/core/lib/jpeg/jpeg_handle.h b/tensorflow/core/lib/jpeg/jpeg_handle.h new file mode 100644 index 0000000000..58f7f6f666 --- /dev/null +++ b/tensorflow/core/lib/jpeg/jpeg_handle.h @@ -0,0 +1,51 @@ +// This file declares the functions and structures for memory I/O with libjpeg +// These functions are not meant to be used directly, see jpeg_mem.h isntead. + +#ifndef TENSORFLOW_LIB_JPEG_JPEG_HANDLE_H_ +#define TENSORFLOW_LIB_JPEG_JPEG_HANDLE_H_ + +extern "C" { +#include "external/jpeg_archive/jpeg-9a/jinclude.h" +#include "external/jpeg_archive/jpeg-9a/jpeglib.h" +#include "external/jpeg_archive/jpeg-9a/jerror.h" +#include "external/jpeg_archive/jpeg-9a/transupp.h" // for rotations +} + +#include "tensorflow/core/platform/port.h" + +namespace tensorflow { +namespace jpeg { + +// Handler for fatal JPEG library errors: clean up & return +void CatchError(j_common_ptr cinfo); + +typedef struct { + struct jpeg_destination_mgr pub; + JOCTET *buffer; + int bufsize; + int datacount; + string *dest; +} MemDestMgr; + +typedef struct { + struct jpeg_source_mgr pub; + const unsigned char *data; + unsigned long int datasize; + bool try_recover_truncated_jpeg; +} MemSourceMgr; + +void SetSrc(j_decompress_ptr cinfo, const void *data, + unsigned long int datasize, bool try_recover_truncated_jpeg); + +// JPEG destination: we will store all the data in a buffer "buffer" of total +// size "bufsize", if the buffer overflows, we will be in trouble. +void SetDest(j_compress_ptr cinfo, void *buffer, int bufsize); +// Same as above, except that buffer is only used as a temporary structure and +// is emptied into "destination" as soon as it fills up. +void SetDest(j_compress_ptr cinfo, void *buffer, int bufsize, + string *destination); + +} // namespace jpeg +} // namespace tensorflow + +#endif // TENSORFLOW_LIB_JPEG_JPEG_HANDLE_H_ diff --git a/tensorflow/core/lib/jpeg/jpeg_mem.cc b/tensorflow/core/lib/jpeg/jpeg_mem.cc new file mode 100644 index 0000000000..556f13e388 --- /dev/null +++ b/tensorflow/core/lib/jpeg/jpeg_mem.cc @@ -0,0 +1,557 @@ +// This file defines functions to compress and uncompress JPEG data +// to and from memory, as well as some direct manipulations of JPEG string + +#include "tensorflow/core/lib/jpeg/jpeg_mem.h" + +#include <setjmp.h> +#include <string.h> +#include <algorithm> +#include <memory> +#include <string> + +#include "tensorflow/core/lib/jpeg/jpeg_handle.h" +#include "tensorflow/core/platform/logging.h" +#include "tensorflow/core/platform/port.h" + +namespace tensorflow { +namespace jpeg { + +// ----------------------------------------------------------------------------- +// Decompression + +namespace { + +enum JPEGErrors { + JPEGERRORS_OK, + JPEGERRORS_UNEXPECTED_END_OF_DATA, + JPEGERRORS_BAD_PARAM +}; + +// Prevent bad compiler behaviour in ASAN mode by wrapping most of the +// arguments in a struct struct. +class FewerArgsForCompiler { + public: + FewerArgsForCompiler(int datasize, const UncompressFlags& flags, int* nwarn, + std::function<uint8*(int, int, int)> allocate_output) + : datasize_(datasize), + flags_(flags), + pnwarn_(nwarn), + allocate_output_(allocate_output), + fraction_read_(0.), + height_(0), + stride_(0) { + if (pnwarn_ != nullptr) *pnwarn_ = 0; + } + + const int datasize_; + const UncompressFlags flags_; + int* const pnwarn_; + std::function<uint8*(int, int, int)> allocate_output_; + float fraction_read_; // fraction of scanline lines successfully read + int height_; + int stride_; +}; + +uint8* UncompressLow(const void* srcdata, FewerArgsForCompiler* argball) { + // unpack the argball + const int datasize = argball->datasize_; + const auto& flags = argball->flags_; + const int ratio = flags.ratio; + int components = flags.components; + int stride = flags.stride; // may be 0 + int* const nwarn = argball->pnwarn_; // may be NULL + + // can't decode if the ratio is not recognized by libjpeg + if ((ratio != 1) && (ratio != 2) && (ratio != 4) && (ratio != 8)) { + return nullptr; + } + + // if empty image, return + if (datasize == 0 || srcdata == NULL) return nullptr; + + // Declare temporary buffer pointer here so that we can free on error paths + JSAMPLE* tempdata = nullptr; + + // Initialize libjpeg structures to have a memory source + // Modify the usual jpeg error manager to catch fatal errors. + JPEGErrors error = JPEGERRORS_OK; + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jmp_buf jpeg_jmpbuf; + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = CatchError; + if (setjmp(jpeg_jmpbuf)) { + return nullptr; + } + + jpeg_create_decompress(&cinfo); + SetSrc(&cinfo, srcdata, datasize, flags.try_recover_truncated_jpeg); + jpeg_read_header(&cinfo, TRUE); + + // Set components automatically if desired + if (components == 0) components = cinfo.num_components; + + // set grayscale and ratio parameters + switch (components) { + case 1: + cinfo.out_color_space = JCS_GRAYSCALE; + break; + case 3: + case 4: + if (cinfo.jpeg_color_space == JCS_CMYK || + cinfo.jpeg_color_space == JCS_YCCK) { + // always use cmyk for output in a 4 channel jpeg. libjpeg has a builtin + // decoder. + cinfo.out_color_space = JCS_CMYK; + } else { + cinfo.out_color_space = JCS_RGB; + } + break; + default: + LOG(ERROR) << " Invalid components value " << components << std::endl; + jpeg_destroy_decompress(&cinfo); + return nullptr; + } + cinfo.do_fancy_upsampling = boolean(flags.fancy_upscaling); + cinfo.scale_num = 1; + cinfo.scale_denom = ratio; + // Activating this has a quality/speed trade-off implication: + // cinfo.dct_method = JDCT_IFAST; + + jpeg_start_decompress(&cinfo); + + // check for compatible stride + const int min_stride = cinfo.output_width * components * sizeof(JSAMPLE); + if (stride == 0) { + stride = min_stride; + } else if (stride < min_stride) { + LOG(ERROR) << "Incompatible stride: " << stride << " < " << min_stride; + jpeg_destroy_decompress(&cinfo); + return nullptr; + } + + // Remember stride and height for use in Uncompress + argball->height_ = cinfo.output_height; + argball->stride_ = stride; + + uint8* const dstdata = argball->allocate_output_( + cinfo.output_width, cinfo.output_height, components); + if (dstdata == nullptr) { + jpeg_destroy_decompress(&cinfo); + return nullptr; + } + JSAMPLE* output_line = static_cast<JSAMPLE*>(dstdata); + + // Temporary buffer used for CMYK -> RGB conversion. + const bool use_cmyk = (cinfo.out_color_space == JCS_CMYK); + tempdata = use_cmyk ? new JSAMPLE[cinfo.output_width * 4] : NULL; + + // If there is an error reading a line, this aborts the reading. + // Save the fraction of the image that has been read. + argball->fraction_read_ = 1.0; + while (cinfo.output_scanline < cinfo.output_height) { + int num_lines_read = 0; + if (cinfo.out_color_space == JCS_CMYK) { + num_lines_read = jpeg_read_scanlines(&cinfo, &tempdata, 1); + // Convert CMYK to RGB + for (size_t i = 0; i < cinfo.output_width; ++i) { + int c = tempdata[4 * i + 0]; + int m = tempdata[4 * i + 1]; + int y = tempdata[4 * i + 2]; + int k = tempdata[4 * i + 3]; + int r, g, b; + if (cinfo.saw_Adobe_marker) { + r = (k * c) / 255; + g = (k * m) / 255; + b = (k * y) / 255; + } else { + r = (255 - k) * (255 - c) / 255; + g = (255 - k) * (255 - m) / 255; + b = (255 - k) * (255 - y) / 255; + } + output_line[3 * i + 0] = r; + output_line[3 * i + 1] = g; + output_line[3 * i + 2] = b; + } + } else { + num_lines_read = jpeg_read_scanlines(&cinfo, &output_line, 1); + } + // Handle error cases + if (num_lines_read == 0) { + LOG(ERROR) << "Premature end of JPEG data. Stopped at line " + << cinfo.output_scanline << "/" << cinfo.output_height; + if (!flags.try_recover_truncated_jpeg) { + argball->fraction_read_ = + static_cast<float>(cinfo.output_scanline) / cinfo.output_height; + error = JPEGERRORS_UNEXPECTED_END_OF_DATA; + } else { + for (size_t line = cinfo.output_scanline; line < cinfo.output_height; + ++line) { + if (line == 0) { + // If even the first line is missing, fill with black color + memset(output_line, 0, min_stride); + } else { + // else, just replicate the line above. + memcpy(output_line, output_line - stride, min_stride); + } + output_line += stride; + } + argball->fraction_read_ = 1.0; // consider all lines as read + // prevent error-on-exit in libjpeg: + cinfo.output_scanline = cinfo.output_height; + } + break; + } + DCHECK_EQ(num_lines_read, 1); + TF_ANNOTATE_MEMORY_IS_INITIALIZED(output_line, min_stride); + output_line += stride; + } + delete[] tempdata; + + // Convert the RGB data to RGBA, with alpha set to 0xFF to indicate + // opacity. + // RGBRGBRGB... --> RGBARGBARGBA... + if (components == 4) { + // Start on the last line. + JSAMPLE* scanlineptr = + static_cast<JSAMPLE*>(dstdata + (cinfo.output_height - 1) * stride); + const JSAMPLE kOpaque = -1; // All ones appropriate for JSAMPLE. + const int right_rgb = (cinfo.output_width - 1) * 3; + const int right_rgba = (cinfo.output_width - 1) * 4; + + for (int y = cinfo.output_height; y-- > 0;) { + // We do all the transformations in place, going backwards for each row. + const JSAMPLE* rgb_pixel = scanlineptr + right_rgb; + JSAMPLE* rgba_pixel = scanlineptr + right_rgba; + scanlineptr -= stride; + for (int x = cinfo.output_width; x-- > 0; + rgba_pixel -= 4, rgb_pixel -= 3) { + // We copy the 3 bytes at rgb_pixel into the 4 bytes at rgba_pixel + // The "a" channel is set to be opaque. + rgba_pixel[3] = kOpaque; + rgba_pixel[2] = rgb_pixel[2]; + rgba_pixel[1] = rgb_pixel[1]; + rgba_pixel[0] = rgb_pixel[0]; + } + } + } + + switch (components) { + case 1: + if (cinfo.output_components != 1) { + error = JPEGERRORS_BAD_PARAM; + } + break; + case 3: + case 4: + if (cinfo.out_color_space == JCS_CMYK) { + if (cinfo.output_components != 4) { + error = JPEGERRORS_BAD_PARAM; + } + } else { + if (cinfo.output_components != 3) { + error = JPEGERRORS_BAD_PARAM; + } + } + break; + default: + // will never happen, should be catched by the previous switch + LOG(ERROR) << "Invalid components value " << components << std::endl; + jpeg_destroy_decompress(&cinfo); + return nullptr; + } + + // save number of warnings if requested + if (nwarn != nullptr) { + *nwarn = cinfo.err->num_warnings; + } + + // Handle errors in JPEG + switch (error) { + case JPEGERRORS_OK: + jpeg_finish_decompress(&cinfo); + break; + case JPEGERRORS_UNEXPECTED_END_OF_DATA: + case JPEGERRORS_BAD_PARAM: + jpeg_abort(reinterpret_cast<j_common_ptr>(&cinfo)); + break; + default: + LOG(ERROR) << "Unhandled case " << error; + break; + } + jpeg_destroy_decompress(&cinfo); + + return dstdata; +} + +} // anonymous namespace + +// ----------------------------------------------------------------------------- +// We do the apparently silly thing of packing 5 of the arguments +// into a structure that is then passed to another routine +// that does all the work. The reason is that we want to catch +// fatal JPEG library errors with setjmp/longjmp, and g++ and +// associated libraries aren't good enough to guarantee that 7 +// parameters won't get clobbered by the longjmp. So we help +// it out a little. +uint8* Uncompress(const void* srcdata, int datasize, + const UncompressFlags& flags, int* nwarn, + std::function<uint8*(int, int, int)> allocate_output) { + FewerArgsForCompiler argball(datasize, flags, nwarn, allocate_output); + uint8* const dstdata = UncompressLow(srcdata, &argball); + const float fraction_read = argball.fraction_read_; + if (dstdata == NULL || + fraction_read < std::min(1.0f, flags.min_acceptable_fraction)) { + // Major failure, none or too-partial read returned; get out + return NULL; + } + + // If there was an error in reading the jpeg data, + // set the unread pixels to black + if (fraction_read < 1.0) { + const int first_bad_line = + static_cast<int>(fraction_read * argball.height_); + uint8* start = dstdata + first_bad_line * argball.stride_; + const int nbytes = (argball.height_ - first_bad_line) * argball.stride_; + memset(static_cast<void*>(start), 0, nbytes); + } + + return dstdata; +} + +uint8* Uncompress(const void* srcdata, int datasize, + const UncompressFlags& flags, int* pwidth, int* pheight, + int* pcomponents, int* nwarn) { + uint8* buffer = NULL; + uint8* result = + Uncompress(srcdata, datasize, flags, nwarn, + [=, &buffer](int width, int height, int components) { + if (pwidth != nullptr) *pwidth = width; + if (pheight != nullptr) *pheight = height; + if (pcomponents != nullptr) *pcomponents = components; + buffer = new uint8[height * width * components]; + return buffer; + }); + if (!result) delete[] buffer; + return result; +} + +// ---------------------------------------------------------------------------- +// Computes image information from jpeg header. +// Returns true on success; false on failure. +bool GetImageInfo(const void* srcdata, int datasize, int* width, int* height, + int* components) { + // Init in case of failure + if (width) *width = 0; + if (height) *height = 0; + if (components) *components = 0; + + // If empty image, return + if (datasize == 0 || srcdata == NULL) return false; + + // Initialize libjpeg structures to have a memory source + // Modify the usual jpeg error manager to catch fatal errors. + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + jmp_buf jpeg_jmpbuf; + cinfo.err = jpeg_std_error(&jerr); + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = CatchError; + if (setjmp(jpeg_jmpbuf)) { + return false; + } + + // set up, read header, set image parameters, save size + jpeg_create_decompress(&cinfo); + SetSrc(&cinfo, srcdata, datasize, false); + + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); // required to transfer image size to cinfo + if (width) *width = cinfo.output_width; + if (height) *height = cinfo.output_height; + if (components) *components = cinfo.output_components; + + jpeg_destroy_decompress(&cinfo); + + return true; +} + +// ----------------------------------------------------------------------------- +// Compression + +namespace { +bool CompressInternal(const uint8* srcdata, int width, int height, + const CompressFlags& flags, string* output) { + output->clear(); + const int components = (static_cast<int>(flags.format) & 0xff); + int in_stride = flags.stride; + if (in_stride == 0) { + in_stride = width * (static_cast<int>(flags.format) & 0xff); + } else if (in_stride < width * components) { + LOG(ERROR) << "Incompatible input stride"; + return false; + } + + JOCTET* buffer = 0; + + // NOTE: for broader use xmp_metadata should be made a unicode string + CHECK(srcdata != nullptr); + CHECK(output != nullptr); + // This struct contains the JPEG compression parameters and pointers to + // working space + struct jpeg_compress_struct cinfo; + // This struct represents a JPEG error handler. + struct jpeg_error_mgr jerr; + jmp_buf jpeg_jmpbuf; // recovery point in case of error + + // Step 1: allocate and initialize JPEG compression object + // Use the usual jpeg error manager. + cinfo.err = jpeg_std_error(&jerr); + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = CatchError; + if (setjmp(jpeg_jmpbuf)) { + output->clear(); + delete[] buffer; + return false; + } + + jpeg_create_compress(&cinfo); + + // Step 2: specify data destination + // We allocate a buffer of reasonable size. If we have a small image, just + // estimate the size of the output using the number of bytes of the input. + // If this is getting too big, we will append to the string by chunks of 1MB. + // This seems like a reasonable compromise between performance and memory. + int bufsize = std::min(width * height * components, 1 << 20); + buffer = new JOCTET[bufsize]; + SetDest(&cinfo, buffer, bufsize, output); + + // Step 3: set parameters for compression + cinfo.image_width = width; + cinfo.image_height = height; + switch (components) { + case 1: + cinfo.input_components = 1; + cinfo.in_color_space = JCS_GRAYSCALE; + break; + case 3: + case 4: + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + break; + default: + LOG(ERROR) << " Invalid components value " << components << std::endl; + output->clear(); + delete[] buffer; + return false; + } + jpeg_set_defaults(&cinfo); + if (flags.optimize_jpeg_size) cinfo.optimize_coding = TRUE; + + cinfo.density_unit = flags.density_unit; // JFIF code for pixel size units: + // 1 = in, 2 = cm + cinfo.X_density = flags.x_density; // Horizontal pixel density + cinfo.Y_density = flags.y_density; // Vertical pixel density + jpeg_set_quality(&cinfo, flags.quality, TRUE); + + if (flags.progressive) { + jpeg_simple_progression(&cinfo); + } + + if (!flags.chroma_downsampling) { + // Turn off chroma subsampling (it is on by default). For more details on + // chroma subsampling, see http://en.wikipedia.org/wiki/Chroma_subsampling. + for (int i = 0; i < cinfo.num_components; ++i) { + cinfo.comp_info[i].h_samp_factor = 1; + cinfo.comp_info[i].v_samp_factor = 1; + } + } + + jpeg_start_compress(&cinfo, TRUE); + + // Embed XMP metadata if any + if (!flags.xmp_metadata.empty()) { + // XMP metadata is embedded in the APP1 tag of JPEG and requires this + // namespace header string (null-terminated) + const string name_space = "http://ns.adobe.com/xap/1.0/"; + const int name_space_length = name_space.size(); + const int metadata_length = flags.xmp_metadata.size(); + const int packet_length = metadata_length + name_space_length + 1; + std::unique_ptr<JOCTET[]> joctet_packet(new JOCTET[packet_length]); + + for (int i = 0; i < name_space_length; i++) { + // Conversion char --> JOCTET + joctet_packet[i] = name_space[i]; + } + joctet_packet[name_space_length] = 0; // null-terminate namespace string + + for (int i = 0; i < metadata_length; i++) { + // Conversion char --> JOCTET + joctet_packet[i + name_space_length + 1] = flags.xmp_metadata[i]; + } + jpeg_write_marker(&cinfo, JPEG_APP0 + 1, joctet_packet.get(), + packet_length); + } + + // JSAMPLEs per row in image_buffer + std::unique_ptr<JSAMPLE[]> row_temp( + new JSAMPLE[width * cinfo.input_components]); + while (cinfo.next_scanline < cinfo.image_height) { + JSAMPROW row_pointer[1]; // pointer to JSAMPLE row[s] + const uint8* r = &srcdata[cinfo.next_scanline * in_stride]; + uint8* p = static_cast<uint8*>(row_temp.get()); + switch (flags.format) { + case FORMAT_RGBA: { + for (int i = 0; i < width; ++i, p += 3, r += 4) { + p[0] = r[0]; + p[1] = r[1]; + p[2] = r[2]; + } + row_pointer[0] = row_temp.get(); + break; + } + case FORMAT_ABGR: { + for (int i = 0; i < width; ++i, p += 3, r += 4) { + p[0] = r[3]; + p[1] = r[2]; + p[2] = r[1]; + } + row_pointer[0] = row_temp.get(); + break; + } + default: { + row_pointer[0] = reinterpret_cast<JSAMPLE*>(const_cast<JSAMPLE*>(r)); + } + } + CHECK_EQ(jpeg_write_scanlines(&cinfo, row_pointer, 1), 1); + } + jpeg_finish_compress(&cinfo); + + // release JPEG compression object + jpeg_destroy_compress(&cinfo); + delete[] buffer; + return true; +} + +} // anonymous namespace + +// ----------------------------------------------------------------------------- + +bool Compress(const void* srcdata, int width, int height, + const CompressFlags& flags, string* output) { + return CompressInternal(static_cast<const uint8*>(srcdata), width, height, + flags, output); +} + +string Compress(const void* srcdata, int width, int height, + const CompressFlags& flags) { + string temp; + CompressInternal(static_cast<const uint8*>(srcdata), width, height, flags, + &temp); + // If CompressInternal fails, temp will be empty. + return temp; +} + +} // namespace jpeg +} // namespace tensorflow diff --git a/tensorflow/core/lib/jpeg/jpeg_mem.h b/tensorflow/core/lib/jpeg/jpeg_mem.h new file mode 100644 index 0000000000..19ba7d4acf --- /dev/null +++ b/tensorflow/core/lib/jpeg/jpeg_mem.h @@ -0,0 +1,130 @@ +// This file defines functions to compress and uncompress JPEG files +// to and from memory. It provides interfaces for raw images +// (data array and size fields). +// Direct manipulation of JPEG strings are supplied: Flip, Rotate, Crop.. + +#ifndef TENSORFLOW_LIB_JPEG_JPEG_MEM_H_ +#define TENSORFLOW_LIB_JPEG_JPEG_MEM_H_ + +#include <functional> +#include <string> +#include <vector> + +#include "tensorflow/core/platform/port.h" +#include "tensorflow/core/lib/core/stringpiece.h" + +namespace tensorflow { +namespace jpeg { + +// Flags for Uncompress +struct UncompressFlags { + // ratio can be 1, 2, 4, or 8 and represent the denominator for the scaling + // factor (eg ratio = 4 means that the resulting image will be at 1/4 original + // size in both directions). + int ratio = 1; + + // The number of bytes per pixel (1, 3 or 4), or 0 for autodetect. + int components = 0; + + // If true, decoder will use a slower but nicer upscaling of the chroma + // planes (yuv420/422 only). + bool fancy_upscaling = true; + + // If true, will attempt to fill in missing lines of truncated files + bool try_recover_truncated_jpeg = false; + + // The minimum required fraction of lines read before the image is accepted. + float min_acceptable_fraction = 1.0; + + // The distance in bytes from one scanline to the other. Should be at least + // equal to width*components*sizeof(JSAMPLE). If 0 is passed, the stride + // used will be this minimal value. + int stride = 0; +}; + +// Uncompress some raw JPEG data given by the pointer srcdata and the length +// datasize. +// - width and height are the address where to store the size of the +// uncompressed image in pixels. May be nullptr. +// - components is the address where the number of read components are +// stored. This is *output only*: to request a specific number of +// components use flags.components. May be nullptr. +// - nwarn is the address in which to store the number of warnings. +// May be nullptr. +// The function returns a pointer to the raw uncompressed data or NULL if +// there was an error. The caller of the function is responsible for +// freeing the memory (using delete []). +uint8* Uncompress(const void* srcdata, int datasize, + const UncompressFlags& flags, int* width, int* height, + int* components, // Output only: useful with autodetect + int* nwarn); + +// Version of Uncompress that allocates memory via a callback. The callback +// arguments are (width, height, components). If the size is known ahead of +// time this function can return an existing buffer; passing a callback allows +// the buffer to be shaped based on the JPEG header. The caller is responsible +// for freeing the memory *even along error paths*. +uint8* Uncompress(const void* srcdata, int datasize, + const UncompressFlags& flags, int* nwarn, + std::function<uint8*(int, int, int)> allocate_output); + +// Read jpeg header and get image information. Returns true on success. +// The width, height, and components points may be null. +bool GetImageInfo(const void* srcdata, int datasize, int* width, int* height, + int* components); + +// Note: (format & 0xff) = number of components (<=> bytes per pixels) +enum Format { + FORMAT_GRAYSCALE = 0x001, // 1 byte/pixel + FORMAT_RGB = 0x003, // 3 bytes/pixel RGBRGBRGBRGB... + FORMAT_RGBA = 0x004, // 4 bytes/pixel RGBARGBARGBARGBA... + FORMAT_ABGR = 0x104 // 4 bytes/pixel ABGRABGRABGR... +}; + +// Flags for compression +struct CompressFlags { + // Encoding of the input data for compression + Format format; + + // Quality of the compression from 0-100 + int quality = 95; + + // If true, create a jpeg image that loads progressively + bool progressive = false; + + // If true, reduce jpeg size without changing quality (at the cost of CPU/RAM) + bool optimize_jpeg_size = false; + + // See http://en.wikipedia.org/wiki/Chroma_subsampling + bool chroma_downsampling = true; + + // Resolution + int density_unit = 1; // 1 = in, 2 = cm + int x_density = 300; + int y_density = 300; + + // If not empty, embed this XMP metadata in the image header + StringPiece xmp_metadata; + + // The distance in bytes from one scanline to the other. Should be at least + // equal to width*components*sizeof(JSAMPLE). If 0 is passed, the stride + // used will be this minimal value. + int stride = 0; +}; + +// Compress some raw image given in srcdata, the data is a 2D array of size +// stride*height with one of the formats enumerated above. +// The encoded data is returned as a string. +// If not empty, XMP metadata can be embedded in the image header +// On error, returns the empty string (which is never a valid jpeg). +string Compress(const void* srcdata, int width, int height, + const CompressFlags& flags); + +// On error, returns false and sets output to empty. +bool Compress(const void* srcdata, int width, int height, + const CompressFlags& flags, string* output); + +} // namespace jpeg +} // namespace tensorflow + +#endif // TENSORFLOW_LIB_JPEG_JPEG_MEM_H_ diff --git a/tensorflow/core/lib/jpeg/jpeg_mem_unittest.cc b/tensorflow/core/lib/jpeg/jpeg_mem_unittest.cc new file mode 100644 index 0000000000..23e72f9d57 --- /dev/null +++ b/tensorflow/core/lib/jpeg/jpeg_mem_unittest.cc @@ -0,0 +1,304 @@ +#include "tensorflow/core/lib/jpeg/jpeg_mem.h" + +#include <setjmp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <memory> + +#include "tensorflow/core/lib/jpeg/jpeg_handle.h" +#include "tensorflow/core/platform/logging.h" +#include "tensorflow/core/platform/port.h" +#include "tensorflow/core/public/env.h" +#include <gtest/gtest.h> + +#include "tensorflow/core/lib/core/casts.h" + +namespace tensorflow { +namespace jpeg { +namespace { + +const char kTestData[] = "tensorflow/core/lib/jpeg/testdata/"; + +int ComputeSumAbsoluteDifference(const uint8* a, const uint8* b, int width, + int height, int a_stride, int b_stride) { + int totalerr = 0; + for (int i = 0; i < height; i++) { + const uint8* const pa = a + i * a_stride; + const uint8* const pb = b + i * b_stride; + for (int j = 0; j < 3 * width; j++) { + totalerr += abs(static_cast<int>(pa[j]) - static_cast<int>(pb[j])); + } + } + return totalerr; +} + +// Reads the contents of the file into output +void ReadFileToStringOrDie(Env* env, const string& filename, string* output) { + TF_CHECK_OK(ReadFileToString(env, filename, output)); +} + +void TestJPEG(Env* env, const string& jpegfile) { + // Read the data from the jpeg file into memory + string jpeg; + ReadFileToStringOrDie(Env::Default(), jpegfile, &jpeg); + const int fsize = jpeg.size(); + const uint8* const temp = bit_cast<const uint8*>(jpeg.data()); + + // try partial decoding (half of the data) + int w, h, c; + std::unique_ptr<uint8[]> imgdata; + + UncompressFlags flags; + flags.components = 3; + + // set min_acceptable_fraction to something insufficient + flags.min_acceptable_fraction = 0.8; + imgdata.reset(Uncompress(temp, fsize / 2, flags, &w, &h, &c, NULL)); + CHECK(imgdata.get() == NULL); + + // now, use a value that makes fsize/2 be enough for a black-filling + flags.min_acceptable_fraction = 0.01; + imgdata.reset(Uncompress(temp, fsize / 2, flags, &w, &h, &c, NULL)); + CHECK(imgdata.get() != NULL); + + // finally, uncompress the whole data + flags.min_acceptable_fraction = 1.0; + imgdata.reset(Uncompress(temp, fsize, flags, &w, &h, &c, NULL)); + CHECK(imgdata.get() != NULL); + + // Uncompress the data to RGBA, too + flags.min_acceptable_fraction = 1.0; + flags.components = 4; + imgdata.reset(Uncompress(temp, fsize, flags, &w, &h, &c, NULL)); + CHECK(imgdata.get() != NULL); +} + +TEST(JpegMemTest, Jpeg) { + Env* env = Env::Default(); + const string data_path = kTestData; + + // Name of a valid jpeg file on the disk + TestJPEG(env, data_path + "jpeg_merge_test1.jpg"); + + // Exercise CMYK machinery as well + TestJPEG(env, data_path + "jpeg_merge_test1_cmyk.jpg"); +} + +TEST(JpegMemTest, Jpeg2) { + // create known data, for size in_w x in_h + const int in_w = 256; + const int in_h = 256; + const int stride1 = 3 * in_w; + const std::unique_ptr<uint8[]> refdata1(new uint8[stride1 * in_h]); + for (int i = 0; i < in_h; i++) { + for (int j = 0; j < in_w; j++) { + const int offset = i * stride1 + 3 * j; + refdata1[offset + 0] = i; + refdata1[offset + 1] = j; + refdata1[offset + 2] = static_cast<uint8>((i + j) >> 1); + } + } + + // duplicate with weird input stride + const int stride2 = 3 * 357; + const std::unique_ptr<uint8[]> refdata2(new uint8[stride2 * in_h]); + for (int i = 0; i < in_h; i++) { + memcpy(&refdata2[i * stride2], &refdata1[i * stride1], 3 * in_w); + } + + // Test compression + string cpdata1, cpdata2; + { + const string kXMP = "XMP_TEST_123"; + + // Compress it to JPEG + CompressFlags flags; + flags.format = FORMAT_RGB; + flags.quality = 97; + flags.xmp_metadata = kXMP; + cpdata1 = Compress(refdata1.get(), in_w, in_h, flags); + flags.stride = stride2; + cpdata2 = Compress(refdata2.get(), in_w, in_h, flags); + // Different input stride shouldn't change the output + CHECK_EQ(cpdata1, cpdata2); + + // Verify valid XMP. + CHECK_NE(string::npos, cpdata1.find(kXMP)); + + // Test the other API, where a storage string is supplied + string cptest; + flags.stride = 0; + Compress(refdata1.get(), in_w, in_h, flags, &cptest); + CHECK_EQ(cptest, cpdata1); + flags.stride = stride2; + Compress(refdata2.get(), in_w, in_h, flags, &cptest); + CHECK_EQ(cptest, cpdata2); + } + + // Uncompress twice: once with 3 components and once with autodetect + std::unique_ptr<uint8[]> imgdata1; + for (const int components : {0, 3}) { + // Uncompress it + UncompressFlags flags; + flags.components = components; + int w, h, c; + imgdata1.reset( + Uncompress(cpdata1.c_str(), cpdata1.length(), flags, &w, &h, &c, NULL)); + + // Check obvious formatting stuff + CHECK_EQ(w, in_w); + CHECK_EQ(h, in_h); + CHECK_EQ(c, 3); + CHECK(imgdata1.get()); + + // Compare the two images + const int totalerr = ComputeSumAbsoluteDifference( + imgdata1.get(), refdata1.get(), in_w, in_h, stride1, stride1); + CHECK_LE(totalerr, 85000); + } + + // check the second image too. Should be bitwise identical to the first. + // uncompress using a weird stride + { + UncompressFlags flags; + flags.stride = 3 * 411; + const std::unique_ptr<uint8[]> imgdata2(new uint8[flags.stride * in_h]); + CHECK(imgdata2.get() == Uncompress(cpdata2.c_str(), cpdata2.length(), flags, + NULL, [&imgdata2](int w, int h, int c) { + CHECK_EQ(w, in_w); + CHECK_EQ(h, in_h); + CHECK_EQ(c, 3); + return imgdata2.get(); + })); + const int totalerr = ComputeSumAbsoluteDifference( + imgdata1.get(), imgdata2.get(), in_w, in_h, stride1, flags.stride); + CHECK_EQ(totalerr, 0); + } +} + +// Takes JPEG data and reads its headers to determine whether or not the JPEG +// was chroma downsampled. +bool IsChromaDownsampled(const string& jpegdata) { + // Initialize libjpeg structures to have a memory source + // Modify the usual jpeg error manager to catch fatal errors. + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + jmp_buf jpeg_jmpbuf; + cinfo.err = jpeg_std_error(&jerr); + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = CatchError; + if (setjmp(jpeg_jmpbuf)) return false; + + // set up, read header, set image parameters, save size + jpeg_create_decompress(&cinfo); + SetSrc(&cinfo, jpegdata.c_str(), jpegdata.size(), false); + + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); // required to transfer image size to cinfo + const int components = cinfo.output_components; + if (components == 1) return false; + + // Check validity + CHECK_EQ(3, components); + CHECK_EQ(cinfo.comp_info[1].h_samp_factor, cinfo.comp_info[2].h_samp_factor) + << "The h sampling factors should be the same."; + CHECK_EQ(cinfo.comp_info[1].v_samp_factor, cinfo.comp_info[2].v_samp_factor) + << "The v sampling factors should be the same."; + for (int i = 0; i < components; ++i) { + CHECK_GT(cinfo.comp_info[i].h_samp_factor, 0) << "Invalid sampling factor."; + CHECK_EQ(cinfo.comp_info[i].h_samp_factor, cinfo.comp_info[i].v_samp_factor) + << "The sampling factor should be the same in both directions."; + } + + // We're downsampled if we use fewer samples for color than for brightness. + // Do this before deallocating cinfo. + const bool downsampled = + cinfo.comp_info[1].h_samp_factor < cinfo.comp_info[0].h_samp_factor; + + jpeg_destroy_decompress(&cinfo); + return downsampled; +} + +TEST(JpegMemTest, ChromaDownsampling) { + // Read the data from a test jpeg file into memory + const string jpegfile = string(kTestData) + "jpeg_merge_test1.jpg"; + string jpeg; + ReadFileToStringOrDie(Env::Default(), jpegfile, &jpeg); + + // Verify that compressing the JPEG with chroma downsampling works. + // + // First, uncompress the JPEG. + UncompressFlags unflags; + unflags.components = 3; + int w, h, c, num_warnings; + std::unique_ptr<uint8[]> uncompressed(Uncompress( + jpeg.c_str(), jpeg.size(), unflags, &w, &h, &c, &num_warnings)); + CHECK(uncompressed.get() != NULL); + CHECK_EQ(num_warnings, 0); + + // Recompress the JPEG with and without chroma downsampling + for (const bool downsample : {false, true}) { + CompressFlags flags; + flags.format = FORMAT_RGB; + flags.quality = 85; + flags.chroma_downsampling = downsample; + string recompressed; + Compress(uncompressed.get(), w, h, flags, &recompressed); + CHECK(!recompressed.empty()); + CHECK_EQ(IsChromaDownsampled(recompressed), downsample); + } +} + +void 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) { + string jpeg; + ReadFileToStringOrDie(env, bad_jpeg_file, &jpeg); + + UncompressFlags flags; + flags.components = 3; + flags.try_recover_truncated_jpeg = try_recover_truncated_jpeg; + + int width, height, components; + std::unique_ptr<uint8[]> imgdata; + imgdata.reset(Uncompress(jpeg.c_str(), jpeg.size(), flags, &width, &height, + &components, NULL)); + if (expected_width > 0) { // we expect the file to decode into 'something' + CHECK_EQ(width, expected_width); + CHECK_EQ(height, expected_height); + CHECK_EQ(components, 3); + CHECK(imgdata.get()); + if (!reference_RGB_file.empty()) { + string ref; + ReadFileToStringOrDie(env, reference_RGB_file, &ref); + CHECK(!memcmp(ref.data(), imgdata.get(), ref.size())); + } + } else { // no decodable + CHECK(!imgdata.get()) << "file:" << bad_jpeg_file; + } +} + +TEST(JpegMemTest, BadJpeg) { + Env* env = Env::Default(); + const string data_path = kTestData; + + // Test corrupt file + TestBadJPEG(env, data_path + "bad_huffman.jpg", 1024, 768, "", false); + TestBadJPEG(env, data_path + "corrupt.jpg", 0 /*120*/, 90, "", false); + + // Truncated files, undecodable because of missing lines: + TestBadJPEG(env, data_path + "corrupt34_2.jpg", 0, 3300, "", false); + TestBadJPEG(env, data_path + "corrupt34_3.jpg", 0, 3300, "", false); + TestBadJPEG(env, data_path + "corrupt34_4.jpg", 0, 3300, "", false); + + // Try in 'recover' mode now: + TestBadJPEG(env, data_path + "corrupt34_2.jpg", 2544, 3300, "", true); + TestBadJPEG(env, data_path + "corrupt34_3.jpg", 2544, 3300, "", true); + TestBadJPEG(env, data_path + "corrupt34_4.jpg", 2544, 3300, "", true); +} + +} // namespace +} // namespace jpeg +} // namespace tensorflow diff --git a/tensorflow/core/lib/jpeg/testdata/bad_huffman.jpg b/tensorflow/core/lib/jpeg/testdata/bad_huffman.jpg Binary files differnew file mode 100644 index 0000000000..ef5b6f12c5 --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/bad_huffman.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/corrupt.jpg b/tensorflow/core/lib/jpeg/testdata/corrupt.jpg Binary files differnew file mode 100644 index 0000000000..5e2fe6c56f --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/corrupt.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/corrupt34_2.jpg b/tensorflow/core/lib/jpeg/testdata/corrupt34_2.jpg Binary files differnew file mode 100644 index 0000000000..4211155c45 --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/corrupt34_2.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/corrupt34_3.jpg b/tensorflow/core/lib/jpeg/testdata/corrupt34_3.jpg Binary files differnew file mode 100644 index 0000000000..c1c2a9d1e1 --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/corrupt34_3.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/corrupt34_4.jpg b/tensorflow/core/lib/jpeg/testdata/corrupt34_4.jpg Binary files differnew file mode 100644 index 0000000000..b8e7308ba0 --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/corrupt34_4.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1.jpg b/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1.jpg Binary files differnew file mode 100644 index 0000000000..5e348a12fd --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1.jpg diff --git a/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1_cmyk.jpg b/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1_cmyk.jpg Binary files differnew file mode 100644 index 0000000000..15f895960d --- /dev/null +++ b/tensorflow/core/lib/jpeg/testdata/jpeg_merge_test1_cmyk.jpg |