on
compressing an image with MozJPEG: full example using jpeg_stdio_src and jpeg_stdio_dest
I was recently looking for a full example of compressing an image programmatically using MozJPEG, without success. The example provided in the official repo is good and does outline all the important steps, but it is missing an import part: how to get the input image metadata and how to read the input image into a memory buffer. It took me few days to figure out the right function calls, so I thought this may be worth sharing.
Compression steps:
According to the example.txt
in the MozJPEG repo, the steps to compress an image programatically are roughly:
GLOBAL(void) write_JPEG_file(char *filename, int quality)
{
/* Step 1: allocate and initialize JPEG compression object */
cinfo.err = jpeg_std_error(&jerr);
/* Now we can initialize the JPEG compression object. */
jpeg_create_compress(&cinfo);
/* Step 2: specify data destination (eg, a file) */
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
jpeg_stdio_dest(&cinfo, outfile);
/* Step 3: set parameters for compression */
cinfo.image_width = image_width; /* image width and height, in pixels */
cinfo.image_height = image_height;
cinfo.input_components = 3; /* # of color components per pixel */
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
/* Step 4: Start compressor */
jpeg_start_compress(&cinfo, TRUE);
/* Step 5: while (scan lines remain to be written) */
/* jpeg_write_scanlines(...); */
row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
while (cinfo.next_scanline < cinfo.image_height) {
/* jpeg_write_scanlines expects an array of pointers to scanlines.
* Here the array is only one element long, but you could pass
* more than one scanline at a time if that's more convenient.
*/
row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride];
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
/* Step 6: Finish compression */
jpeg_finish_compress(&cinfo);
/* After finish_compress, we can close the output file. */
fclose(outfile);
/* This is an important step since it will release a good deal of memory. */
jpeg_destroy_compress(&cinfo);
/* And we're done! */
}
So far so good. If you look carefully at the example.txt, you will see the following global variables:
extern JSAMPLE *image_buffer; /* Points to large array of R,G,B-order data */
extern int image_height; /* Number of rows in image */
extern int image_width; /* Number of columns in image */
these info or metadata are necessary to compress the image; however, the example assumes that they are set somewhere else, so they are marked as extern. The problem is that we don’t know how they are obtained. Not nice at all. Also, the example assumes that the color space of the image is RGB and the consequently the number of color components is 3. We need to get those dynamically from the input image.
The missing pieces:
After some digging, it seems like the right way to obtain the needed info about the input image is by decompressing it with the help of the jpeg_decompress_struct
. An image can be decompressed as follows:
struct jpeg_decompress_struct dinfo;
struct jpeg_error_mgr jerr2;
dinfo.err = jpeg_std_error(&jerr2);
jpeg_create_decompress(&dinfo);
jpeg_stdio_src(&dinfo, input_file);;
FILE *output = fopen(dst_file_name, "wb");
jpeg_stdio_dest(&cinfo, output);
jpeg_save_markers(&dinfo, JPEG_COM, 0xFFFF);
for (int m = 0; m < 16; m++)
jpeg_save_markers(&dinfo, JPEG_APP0 + m, 0xFFFF);
jpeg_read_header(&dinfo, TRUE);
jpeg_start_decompress(&dinfo);
Once jpeg_start_decompress
is called, the image metadata that we need is stored into dinfo
, so we need to get those info and set them into our cinfo
which is of type jpeg_compress_struct
:
cinfo.in_color_space = dinfo.out_color_space;
cinfo.input_components = dinfo.output_components;
cinfo.data_precision = dinfo.data_precision;
cinfo.image_width = dinfo.image_width;
cinfo.image_height = dinfo.image_height;
cinfo.raw_data_in = FALSE;
An additional step before starting the jpeg_write_scanlines
loop is to allocate the image_buffer
that will be used to carry data temporarily when jpeg_read_scanlines
is called:
JSAMPARRAY image_buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, (JDIMENSION) (cinfo.image_width * cinfo.input_components), (JDIMENSION) 1);
All these steps can be found in the method start_input_jpeg
in rdjpeg.c. The tricky part was: start_input_jpeg
is not part of the standard MozJPEG API, so it is not present in the jpeglib.h
. Therefore, I needed to identify the necessary statements and copy them.
Now we are ready to start compressing. We need to get the decompressed data using jpeg_read_scanlines
and write it to the resulting file after compressing it using jpeg_write_scanlines
:
while (cinfo.next_scanline < cinfo.image_height) {
int numscanlines = jpeg_read_scanlines(&dinfo, image_buffer, 1);
jpeg_write_scanlines(&cinfo, image_buffer, numscanlines);
}
This last step was also missing from the example.txt as it was assumed that image_buffer
was containing the image data already.
Stiching it all together:
void compress_JPEG_file(char *src_file_name, char* dst_file_name, int quality) {
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *input_file;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
//needed otherwise jpeg_set_defaults fails, we will set the real one later
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
if ((input_file = fopen(src_file_name, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", src_file_name);
exit(1);
}
struct jpeg_decompress_struct dinfo;
struct jpeg_error_mgr jerr2;
dinfo.err = jpeg_std_error(&jerr2);
jpeg_create_decompress(&dinfo);
jpeg_stdio_src(&dinfo, input_file);;
FILE *output = fopen(dst_file_name, "wb");
jpeg_stdio_dest(&cinfo, output);
jpeg_save_markers(&dinfo, JPEG_COM, 0xFFFF);
for (int m = 0; m < 16; m++)
jpeg_save_markers(&dinfo, JPEG_APP0 + m, 0xFFFF);
jpeg_read_header(&dinfo, TRUE);
jpeg_start_decompress(&dinfo);
cinfo.in_color_space = dinfo.out_color_space;
cinfo.input_components = dinfo.output_components;
cinfo.data_precision = dinfo.data_precision;
cinfo.image_width = dinfo.image_width;
cinfo.image_height = dinfo.image_height;
cinfo.raw_data_in = FALSE;
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
JSAMPARRAY image_buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, (JDIMENSION) (cinfo.image_width * cinfo.input_components), (JDIMENSION) 1);
while (cinfo.next_scanline < cinfo.image_height) {
int numscanlines = jpeg_read_scanlines(&dinfo, image_buffer, 1);
jpeg_write_scanlines(&cinfo, image_buffer, numscanlines);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
jpeg_finish_decompress(&dinfo);
jpeg_destroy_decompress(&dinfo);
fclose(input_file);
fclose(output);
}
You can find the full example code with the main function in this gist
Compiling:
To compress the example above you need to have MozJPEG installed. Assuming the host is Linux based, the command to compile:
gcc -Wall -o compress -I/opt/mozjpeg/include/ -L/opt/mozjpeg/lib64 compress.c -lturbojpeg -ljpeg -Wl,-rpath=/opt/mozjpeg/lib64/