Permalink
Fetching contributors…
Cannot retrieve contributors at this time
733 lines (591 sloc) 19.9 KB
/*
=head1 NAME
jpeg.c - implement saving and loading JPEG images
=head1 SYNOPSIS
io_glue *ig;
if (!i_writejpeg_wiol(im, ig, quality)) {
.. error ..
}
im = i_readjpeg_wiol(ig, length, iptc_text, itlength);
=head1 DESCRIPTION
Reads and writes JPEG images
=over
=cut
*/
#include "imjpeg.h"
#include "imext.h"
#include <stdio.h>
#include <sys/stat.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif
#include <setjmp.h>
#include <string.h>
#include "jpeglib.h"
#include "jerror.h"
#include <errno.h>
#include <stdlib.h>
#include "imexif.h"
#define JPEG_APP13 0xED /* APP13 marker code */
#define JPEG_APP1 (JPEG_APP0 + 1)
#define JPGS 16384
#define JPEG_DIM_MAX JPEG_MAX_DIMENSION
#define _STRINGIFY(x) #x
#define STRINGIFY(x) _STRINGIFY(x)
static unsigned char fake_eoi[]={(JOCTET) 0xFF,(JOCTET) JPEG_EOI};
/* Source and Destination managers */
typedef struct {
struct jpeg_source_mgr pub; /* public fields */
io_glue *data;
JOCTET *buffer; /* start of buffer */
int length; /* Do I need this? */
boolean start_of_file; /* have we gotten any data yet? */
} wiol_source_mgr;
typedef struct {
struct jpeg_destination_mgr pub; /* public fields */
io_glue *data;
JOCTET *buffer; /* start of buffer */
boolean start_of_file; /* have we gotten any data yet? */
} wiol_destination_mgr;
typedef wiol_source_mgr *wiol_src_ptr;
typedef wiol_destination_mgr *wiol_dest_ptr;
/*
* Methods for io manager objects
*
* Methods for source managers:
*
* init_source (j_decompress_ptr cinfo);
* skip_input_data (j_decompress_ptr cinfo, long num_bytes);
* fill_input_buffer (j_decompress_ptr cinfo);
* term_source (j_decompress_ptr cinfo);
*/
static void
wiol_init_source (j_decompress_ptr cinfo) {
wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
/* We reset the empty-input-file flag for each image, but we don't clear
* the input buffer. This is correct behavior for reading a series of
* images from one source.
*/
src->start_of_file = TRUE;
}
static boolean
wiol_fill_input_buffer(j_decompress_ptr cinfo) {
wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
ssize_t nbytes; /* We assume that reads are "small" */
mm_log((1,"wiol_fill_input_buffer(cinfo %p)\n", cinfo));
nbytes = i_io_read(src->data, src->buffer, JPGS);
if (nbytes <= 0) { /* Insert a fake EOI marker */
src->pub.next_input_byte = fake_eoi;
src->pub.bytes_in_buffer = 2;
} else {
src->pub.next_input_byte = src->buffer;
src->pub.bytes_in_buffer = nbytes;
}
src->start_of_file = FALSE;
return TRUE;
}
static void
wiol_skip_input_data (j_decompress_ptr cinfo, long num_bytes) {
wiol_src_ptr src = (wiol_src_ptr) cinfo->src;
/* Just a dumb implementation for now. Could use fseek() except
* it doesn't work on pipes. Not clear that being smart is worth
* any trouble anyway --- large skips are infrequent.
*/
if (num_bytes > 0) {
while (num_bytes > (long) src->pub.bytes_in_buffer) {
num_bytes -= (long) src->pub.bytes_in_buffer;
(void) wiol_fill_input_buffer(cinfo);
/* note we assume that fill_input_buffer will never return FALSE,
* so suspension need not be handled.
*/
}
src->pub.next_input_byte += (size_t) num_bytes;
src->pub.bytes_in_buffer -= (size_t) num_bytes;
}
}
static void
wiol_term_source (j_decompress_ptr cinfo) {
/* no work necessary here */
wiol_src_ptr src;
if (cinfo->src != NULL) {
src = (wiol_src_ptr) cinfo->src;
myfree(src->buffer);
}
}
/* Source manager Constructor */
static void
jpeg_wiol_src(j_decompress_ptr cinfo, io_glue *ig, int length) {
wiol_src_ptr src;
if (cinfo->src == NULL) { /* first time for this JPEG object? */
cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small)
((j_common_ptr) cinfo, JPOOL_PERMANENT,sizeof(wiol_source_mgr));
src = (wiol_src_ptr) cinfo->src;
}
/* put the request method call in here later */
src = (wiol_src_ptr) cinfo->src;
src->data = ig;
src->buffer = mymalloc( JPGS );
src->length = length;
src->pub.init_source = wiol_init_source;
src->pub.fill_input_buffer = wiol_fill_input_buffer;
src->pub.skip_input_data = wiol_skip_input_data;
src->pub.resync_to_restart = jpeg_resync_to_restart; /* use default method */
src->pub.term_source = wiol_term_source;
src->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */
src->pub.next_input_byte = NULL; /* until buffer loaded */
}
/*
* Methods for destination managers:
*
* init_destination (j_compress_ptr cinfo);
* empty_output_buffer (j_compress_ptr cinfo);
* term_destination (j_compress_ptr cinfo);
*
*/
static void
wiol_init_destination (j_compress_ptr cinfo) {
wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
/* We reset the empty-input-file flag for each image, but we don't clear
* the input buffer. This is correct behavior for reading a series of
* images from one source.
*/
dest->start_of_file = TRUE;
}
static boolean
wiol_empty_output_buffer(j_compress_ptr cinfo) {
wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
ssize_t rc;
/*
Previously this code was checking free_in_buffer to see how much
needed to be written. This does not follow the documentation:
"In typical applications, it should write out the
*entire* buffer (use the saved start address and buffer length;
ignore the current state of next_output_byte and free_in_buffer)."
ssize_t nbytes = JPGS - dest->pub.free_in_buffer;
*/
mm_log((1,"wiol_empty_output_buffer(cinfo %p)\n", cinfo));
rc = i_io_write(dest->data, dest->buffer, JPGS);
if (rc != JPGS) { /* XXX: Should raise some jpeg error */
myfree(dest->buffer);
mm_log((1, "wiol_empty_output_buffer: Error: nbytes = %d != rc = %d\n", JPGS, (int)rc));
ERREXIT(cinfo, JERR_FILE_WRITE);
}
dest->pub.free_in_buffer = JPGS;
dest->pub.next_output_byte = dest->buffer;
return TRUE;
}
static void
wiol_term_destination (j_compress_ptr cinfo) {
wiol_dest_ptr dest = (wiol_dest_ptr) cinfo->dest;
size_t nbytes = JPGS - dest->pub.free_in_buffer;
/* yes, this needs to flush the buffer */
/* needs error handling */
if (i_io_write(dest->data, dest->buffer, nbytes) != nbytes) {
myfree(dest->buffer);
ERREXIT(cinfo, JERR_FILE_WRITE);
}
if (dest != NULL) myfree(dest->buffer);
}
/* Destination manager Constructor */
static void
jpeg_wiol_dest(j_compress_ptr cinfo, io_glue *ig) {
wiol_dest_ptr dest;
if (cinfo->dest == NULL) { /* first time for this JPEG object? */
cinfo->dest =
(struct jpeg_destination_mgr *)
(*cinfo->mem->alloc_small)
((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(wiol_destination_mgr));
}
dest = (wiol_dest_ptr) cinfo->dest;
dest->data = ig;
dest->buffer = mymalloc( JPGS );
dest->pub.init_destination = wiol_init_destination;
dest->pub.empty_output_buffer = wiol_empty_output_buffer;
dest->pub.term_destination = wiol_term_destination;
dest->pub.free_in_buffer = JPGS;
dest->pub.next_output_byte = dest->buffer;
}
METHODDEF(void)
my_output_message (j_common_ptr cinfo) {
char buffer[JMSG_LENGTH_MAX];
/* Create the message */
(*cinfo->err->format_message) (cinfo, buffer);
i_push_error(0, buffer);
/* Send it to stderr, adding a newline */
mm_log((1, "%s\n", buffer));
}
struct my_error_mgr {
struct jpeg_error_mgr pub; /* "public" fields */
jmp_buf setjmp_buffer; /* for return to caller */
};
typedef struct my_error_mgr * my_error_ptr;
/* Here's the routine that will replace the standard error_exit method */
METHODDEF(void)
my_error_exit (j_common_ptr cinfo) {
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
my_error_ptr myerr = (my_error_ptr) cinfo->err;
/* Always display the message. */
/* We could postpone this until after returning, if we chose. */
(*cinfo->err->output_message) (cinfo);
/* Return control to the setjmp point */
longjmp(myerr->setjmp_buffer, 1);
}
static void
transfer_cmyk_inverted(i_color *out, JSAMPARRAY in, int width) {
JSAMPROW inrow = *in;
while (width--) {
/* extract and convert to real CMYK */
/* horribly enough this is correct given cmyk values are inverted */
int c = *inrow++;
int m = *inrow++;
int y = *inrow++;
int k = *inrow++;
out->rgba.r = (c * k) / MAXJSAMPLE;
out->rgba.g = (m * k) / MAXJSAMPLE;
out->rgba.b = (y * k) / MAXJSAMPLE;
++out;
}
}
static void
transfer_rgb(i_color *out, JSAMPARRAY in, int width) {
JSAMPROW inrow = *in;
while (width--) {
out->rgba.r = *inrow++;
out->rgba.g = *inrow++;
out->rgba.b = *inrow++;
++out;
}
}
static void
transfer_gray(i_color *out, JSAMPARRAY in, int width) {
JSAMPROW inrow = *in;
while (width--) {
out->gray.gray_color = *inrow++;
++out;
}
}
typedef void (*transfer_function_t)(i_color *out, JSAMPARRAY in, int width);
static const char version_string[] =
#ifdef LIBJPEG_TURBO_VERSION
"libjpeg-turbo " STRINGIFY(LIBJPEG_TURBO_VERSION) " api " STRINGIFY(JPEG_LIB_VERSION)
#else
"libjpeg " STRINGIFY(JPEG_LIB_VERSION)
#endif
;
/*
=item i_libjpeg_version()
=cut
*/
const char *
i_libjpeg_version(void) {
return version_string;
}
/*
=item i_readjpeg_wiol(data, length, iptc_itext, itlength)
=cut
*/
i_img*
i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
i_img * volatile im = NULL;
int seen_exif = 0;
i_color * volatile line_buffer = NULL;
struct jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
JSAMPARRAY buffer; /* Output row buffer */
int row_stride; /* physical row width in output buffer */
jpeg_saved_marker_ptr markerp;
transfer_function_t transfer_f;
int channels;
volatile int src_set = 0;
mm_log((1,"i_readjpeg_wiol(data %p, length %d,iptc_itext %p)\n", data, length, iptc_itext));
i_clear_error();
*iptc_itext = NULL;
*itlength = 0;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
jerr.pub.output_message = my_output_message;
/* Set error handler */
if (setjmp(jerr.setjmp_buffer)) {
if (src_set)
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
if (line_buffer)
myfree(line_buffer);
if (im)
i_img_destroy(im);
return NULL;
}
jpeg_create_decompress(&cinfo);
jpeg_save_markers(&cinfo, JPEG_APP13, 0xFFFF);
jpeg_save_markers(&cinfo, JPEG_APP1, 0xFFFF);
jpeg_save_markers(&cinfo, JPEG_COM, 0xFFFF);
jpeg_wiol_src(&cinfo, data, length);
src_set = 1;
(void) jpeg_read_header(&cinfo, TRUE);
(void) jpeg_start_decompress(&cinfo);
channels = cinfo.output_components;
switch (cinfo.out_color_space) {
case JCS_GRAYSCALE:
if (cinfo.output_components != 1) {
mm_log((1, "i_readjpeg: grayscale image with %d channels\n", cinfo.output_components));
i_push_errorf(0, "grayscale image with invalid components %d", cinfo.output_components);
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
transfer_f = transfer_gray;
break;
case JCS_RGB:
transfer_f = transfer_rgb;
if (cinfo.output_components != 3) {
mm_log((1, "i_readjpeg: RGB image with %d channels\n", cinfo.output_components));
i_push_errorf(0, "RGB image with invalid components %d", cinfo.output_components);
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
break;
case JCS_CMYK:
if (cinfo.output_components == 4) {
/* we treat the CMYK values as inverted, because that's what that
buggy photoshop does, and everyone has to follow the gorilla.
Is there any app that still produces correct CMYK JPEGs?
*/
transfer_f = transfer_cmyk_inverted;
channels = 3;
}
else {
mm_log((1, "i_readjpeg: cmyk image with %d channels\n", cinfo.output_components));
i_push_errorf(0, "CMYK image with invalid components %d", cinfo.output_components);
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
break;
default:
mm_log((1, "i_readjpeg: unknown color space %d\n", cinfo.out_color_space));
i_push_errorf(0, "Unknown color space %d", cinfo.out_color_space);
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
if (!i_int_check_image_file_limits(cinfo.output_width, cinfo.output_height,
channels, sizeof(i_sample_t))) {
mm_log((1, "i_readjpeg: image size exceeds limits\n"));
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
im = i_img_8_new(cinfo.output_width, cinfo.output_height, channels);
if (!im) {
wiol_term_source(&cinfo);
jpeg_destroy_decompress(&cinfo);
return NULL;
}
row_stride = cinfo.output_width * cinfo.output_components;
buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
line_buffer = mymalloc(sizeof(i_color) * cinfo.output_width);
while (cinfo.output_scanline < cinfo.output_height) {
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
transfer_f(line_buffer, buffer, cinfo.output_width);
i_plin(im, 0, cinfo.output_width, cinfo.output_scanline-1, line_buffer);
}
myfree(line_buffer);
line_buffer = NULL;
/* check for APP1 marker and save */
markerp = cinfo.marker_list;
while (markerp != NULL) {
if (markerp->marker == JPEG_COM) {
i_tags_set(&im->tags, "jpeg_comment", (const char *)markerp->data,
markerp->data_length);
}
else if (markerp->marker == JPEG_APP1 && !seen_exif) {
seen_exif = i_int_decode_exif(im, markerp->data, markerp->data_length);
}
else if (markerp->marker == JPEG_APP13) {
*iptc_itext = mymalloc(markerp->data_length);
memcpy(*iptc_itext, markerp->data, markerp->data_length);
*itlength = markerp->data_length;
}
markerp = markerp->next;
}
i_tags_setn(&im->tags, "jpeg_out_color_space", cinfo.out_color_space);
i_tags_setn(&im->tags, "jpeg_color_space", cinfo.jpeg_color_space);
if (cinfo.saw_JFIF_marker) {
double xres = cinfo.X_density;
double yres = cinfo.Y_density;
i_tags_setn(&im->tags, "jpeg_density_unit", cinfo.density_unit);
switch (cinfo.density_unit) {
case 0: /* values are just the aspect ratio */
i_tags_setn(&im->tags, "i_aspect_only", 1);
i_tags_set(&im->tags, "jpeg_density_unit_name", "none", -1);
break;
case 1: /* per inch */
i_tags_set(&im->tags, "jpeg_density_unit_name", "inch", -1);
break;
case 2: /* per cm */
i_tags_set(&im->tags, "jpeg_density_unit_name", "centimeter", -1);
xres *= 2.54;
yres *= 2.54;
break;
}
i_tags_set_float2(&im->tags, "i_xres", 0, xres, 6);
i_tags_set_float2(&im->tags, "i_yres", 0, yres, 6);
}
/* I originally used jpeg_has_multiple_scans() here, but that can
* return true for non-progressive files too. The progressive_mode
* member is available at least as far back as 6b and does the right
* thing.
*/
i_tags_setn(&im->tags, "jpeg_progressive",
cinfo.progressive_mode ? 1 : 0);
(void) jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
i_tags_set(&im->tags, "i_format", "jpeg", 4);
mm_log((1,"i_readjpeg_wiol -> (%p)\n",im));
return im;
}
/*
=item i_writejpeg_wiol(im, ig, qfactor)
=cut
*/
undef_int
i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
JSAMPLE *image_buffer;
int quality;
int got_xres, got_yres, aspect_only, resunit;
double xres, yres;
int comment_entry;
int want_channels = im->channels;
int progressive = 0;
int optimize = 0;
struct jpeg_compress_struct cinfo;
struct my_error_mgr jerr;
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
int row_stride; /* physical row width in image buffer */
unsigned char * data = NULL;
i_color *line_buf = NULL;
mm_log((1,"i_writejpeg(im %p, ig %p, qfactor %d)\n", im, ig, qfactor));
i_clear_error();
if (im->xsize > JPEG_DIM_MAX || im->ysize > JPEG_DIM_MAX) {
i_push_error(0, "image too large for JPEG");
return 0;
}
if (!(im->channels==1 || im->channels==3)) {
want_channels = im->channels - 1;
}
quality = qfactor;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
jerr.pub.output_message = my_output_message;
jpeg_create_compress(&cinfo);
if (setjmp(jerr.setjmp_buffer)) {
jpeg_destroy_compress(&cinfo);
if (data)
myfree(data);
if (line_buf)
myfree(line_buf);
return 0;
}
jpeg_wiol_dest(&cinfo, ig);
cinfo.image_width = im -> xsize; /* image width and height, in pixels */
cinfo.image_height = im -> ysize;
if (want_channels==3) {
cinfo.input_components = 3; /* # of color components per pixel */
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
}
if (want_channels==1) {
cinfo.input_components = 1; /* # of color components per pixel */
cinfo.in_color_space = JCS_GRAYSCALE; /* colorspace of input image */
}
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE); /* limit to baseline-JPEG values */
if (!i_tags_get_int(&im->tags, "jpeg_progressive", 0, &progressive))
progressive = 0;
if (progressive) {
jpeg_simple_progression(&cinfo);
}
if (!i_tags_get_int(&im->tags, "jpeg_optimize", 0, &optimize))
optimize = 0;
cinfo.optimize_coding = optimize;
got_xres = i_tags_get_float(&im->tags, "i_xres", 0, &xres);
got_yres = i_tags_get_float(&im->tags, "i_yres", 0, &yres);
if (!i_tags_get_int(&im->tags, "i_aspect_only", 0,&aspect_only))
aspect_only = 0;
if (!i_tags_get_int(&im->tags, "jpeg_density_unit", 0, &resunit))
resunit = 1; /* per inch */
if (resunit < 0 || resunit > 2) /* default to inch if invalid */
resunit = 1;
if (got_xres || got_yres) {
if (!got_xres)
xres = yres;
else if (!got_yres)
yres = xres;
if (aspect_only)
resunit = 0; /* standard tags override format tags */
if (resunit == 2) {
/* convert to per cm */
xres /= 2.54;
yres /= 2.54;
}
cinfo.density_unit = resunit;
cinfo.X_density = (int)(xres + 0.5);
cinfo.Y_density = (int)(yres + 0.5);
}
jpeg_start_compress(&cinfo, TRUE);
if (i_tags_find(&im->tags, "jpeg_comment", 0, &comment_entry)) {
jpeg_write_marker(&cinfo, JPEG_COM,
(const JOCTET *)im->tags.tags[comment_entry].data,
im->tags.tags[comment_entry].size);
}
row_stride = im->xsize * im->channels; /* JSAMPLEs per row in image_buffer */
if (!im->virtual && im->type == i_direct_type && im->bits == i_8_bits
&& im->channels == want_channels) {
image_buffer=im->idata;
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);
}
}
else {
i_color bg;
i_get_file_background(im, &bg);
data = mymalloc(im->xsize * im->channels);
if (data) {
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.
*/
i_gsamp_bg(im, 0, im->xsize, cinfo.next_scanline, data,
want_channels, &bg);
row_pointer[0] = data;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
myfree(data);
}
else {
jpeg_destroy_compress(&cinfo);
i_push_error(0, "out of memory");
return 0; /* out of memory? */
}
}
/* Step 6: Finish compression */
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
if (i_io_close(ig))
return 0;
return(1);
}
/*
=back
=head1 AUTHOR
Arnar M. Hrafnkelsson, addi@umich.edu
=head1 SEE ALSO
Imager(3)
=cut
*/