]> git.imager.perl.org - imager.git/blobdiff - jpeg.c
add i_gsamp_bg/i_gsampf_bg functions, sample based versions of
[imager.git] / jpeg.c
diff --git a/jpeg.c b/jpeg.c
index 5796f0034fb0d328a4b83f2b25a62ee5a64ceb9e..f886df2561e728956d7c9e91ec48143076f4a143 100644 (file)
--- a/jpeg.c
+++ b/jpeg.c
@@ -1,3 +1,25 @@
+/*
+=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 <stdio.h>
 #include <sys/stat.h>
 #ifndef _MSC_VER
 #include <setjmp.h>
 
 #include "iolayer.h"
-#include "image.h"
+#include "imageri.h"
 #include "jpeglib.h"
+#include "jerror.h"
+#include <errno.h>
+#ifdef IMEXIF_ENABLE
+#include "imexif.h"
+#endif
 
 #define JPEG_APP13       0xED    /* APP13 marker code */
-#define JPGS 1024
+#define JPEG_APP1 (JPEG_APP0 + 1)
+#define JPGS 16384
 
-unsigned char fake_eoi[]={(JOCTET) 0xFF,(JOCTET) JPEG_EOI};
+static unsigned char fake_eoi[]={(JOCTET) 0xFF,(JOCTET) JPEG_EOI};
 
 /* Bad design right here */
 
@@ -73,7 +101,7 @@ 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 0x%p)\n"));
+  mm_log((1,"wiol_fill_input_buffer(cinfo 0x%p)\n", cinfo));
   
   nbytes = src->data->readcb(src->data, src->buffer, JPGS);
   
@@ -189,11 +217,13 @@ wiol_empty_output_buffer(j_compress_ptr cinfo) {
   ssize_t nbytes     = JPGS - dest->pub.free_in_buffer;
   */
 
-  mm_log((1,"wiol_emtpy_output_buffer(cinfo 0x%p)\n"));
+  mm_log((1,"wiol_empty_output_buffer(cinfo 0x%p)\n", cinfo));
   rc = dest->data->writecb(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, rc));
+    ERREXIT(cinfo, JERR_FILE_WRITE);
   }
   dest->pub.free_in_buffer = JPGS;
   dest->pub.next_output_byte = dest->buffer;
@@ -203,14 +233,15 @@ wiol_empty_output_buffer(j_compress_ptr cinfo) {
 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 */
-  dest->data->writecb(dest->data, dest->buffer, 
-                     JPGS - dest->pub.free_in_buffer);
 
-  mm_log((1, "wiol_term_destination(cinfo %p)\n", cinfo));
-  mm_log((1, "wiol_term_destination: dest %p\n", cinfo->dest));
+  if (dest->data->writecb(dest->data, dest->buffer, nbytes) != nbytes) {
+    myfree(dest->buffer);
+    ERREXIT(cinfo, JERR_FILE_WRITE);
+  }
+  
   if (dest != NULL) myfree(dest->buffer);
 }
 
@@ -239,10 +270,6 @@ jpeg_wiol_dest(j_compress_ptr cinfo, io_glue *ig) {
   dest->pub.next_output_byte    = dest->buffer;
 }
 
-
-
-
-
 LOCAL(unsigned int)
 jpeg_getc (j_decompress_ptr cinfo)
 /* Read next byte */
@@ -282,6 +309,8 @@ my_output_message (j_common_ptr cinfo) {
   /* 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));
 }
@@ -303,25 +332,74 @@ my_error_exit (j_common_ptr cinfo) {
   /* 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);
 
+/*
+=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 *im;
-
+  i_img * volatile im = NULL;
+#ifdef IMEXIF_ENABLE
+  int seen_exif = 0;
+#endif
+  i_color *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 0x%p, length %d,iptc_itext 0x%p)\n", data, iptc_itext));
+  mm_log((1,"i_readjpeg_wiol(data 0x%p, length %d,iptc_itext 0x%p)\n", data, length, iptc_itext));
+
+  i_clear_error();
 
   iptc_text = iptc_itext;
   cinfo.err = jpeg_std_error(&jerr.pub);
@@ -330,68 +408,216 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
 
   /* Set error handler */
   if (setjmp(jerr.setjmp_buffer)) {
+    if (src_set)
+      wiol_term_source(&cinfo);
     jpeg_destroy_decompress(&cinfo); 
     *iptc_itext=NULL;
     *itlength=0;
+    if (line_buffer)
+      myfree(line_buffer);
+    if (im)
+      i_img_destroy(im);
     return NULL;
   }
   
   jpeg_create_decompress(&cinfo);
   jpeg_set_marker_processor(&cinfo, JPEG_APP13, APP13_handler);
+  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);
-  im=i_img_empty_ch(NULL,cinfo.output_width,cinfo.output_height,cinfo.output_components);
+
+  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_empty_ch(NULL, 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);
-    memcpy(im->idata+im->channels*im->xsize*(cinfo.output_scanline-1),buffer[0],row_stride);
+    transfer_f(line_buffer, buffer, cinfo.output_width);
+    i_plin(im, 0, cinfo.output_width, cinfo.output_scanline-1, line_buffer);
+  }
+  myfree(line_buffer);
+
+  /* check for APP1 marker and save */
+  markerp = cinfo.marker_list;
+  while (markerp != NULL) {
+    if (markerp->marker == JPEG_COM) {
+      i_tags_add(&im->tags, "jpeg_comment", 0, (const char *)markerp->data,
+                markerp->data_length, 0);
+    }
+#ifdef IMEXIF_ENABLE
+    else if (markerp->marker == JPEG_APP1 && !seen_exif) {
+      seen_exif = i_int_decode_exif(im, markerp->data, markerp->data_length);
+    }
+#endif      
+
+    markerp = markerp->next;
   }
+
+  i_tags_addn(&im->tags, "jpeg_out_color_space", 0, cinfo.out_color_space);
+  i_tags_addn(&im->tags, "jpeg_color_space", 0, cinfo.jpeg_color_space);
+
+  if (cinfo.saw_JFIF_marker) {
+    double xres = cinfo.X_density;
+    double yres = cinfo.Y_density;
+    
+    i_tags_addn(&im->tags, "jpeg_density_unit", 0, cinfo.density_unit);
+    switch (cinfo.density_unit) {
+    case 0: /* values are just the aspect ratio */
+      i_tags_addn(&im->tags, "i_aspect_only", 0, 1);
+      i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "none", -1, 0);
+      break;
+
+    case 1: /* per inch */
+      i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "inch", -1, 0);
+      break;
+
+    case 2: /* per cm */
+      i_tags_add(&im->tags, "jpeg_density_unit_name", 0, "centimeter", -1, 0);
+      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);
+  }
+
   (void) jpeg_finish_decompress(&cinfo);
   jpeg_destroy_decompress(&cinfo);
   *itlength=tlength;
+
+  i_tags_add(&im->tags, "i_format", 0, "jpeg", 4, 0);
+
   mm_log((1,"i_readjpeg_wiol -> (0x%x)\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) {
-  struct stat stbuf;
   JSAMPLE *image_buffer;
   int quality;
+  int got_xres, got_yres, aspect_only, resunit;
+  double xres, yres;
+  int comment_entry;
+  int want_channels = im->channels;
 
   struct jpeg_compress_struct cinfo;
-  struct jpeg_error_mgr jerr;
+  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));
   
-  if (!(im->channels==1 || im->channels==3)) { fprintf(stderr,"Unable to write JPEG, improper colorspace.\n"); exit(3); }
-  quality = qfactor;
+  i_clear_error();
+  io_glue_commit_types(ig);
 
-  cinfo.err = jpeg_std_error(&jerr);
+  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);
 
-  io_glue_commit_types(ig);
+  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 (im->channels==3) {
+  if (want_channels==3) {
     cinfo.input_components = 3;                /* # of color components per pixel */
     cinfo.in_color_space = JCS_RGB;    /* colorspace of input image */
   }
 
-  if (im->channels==1) {
+  if (want_channels==1) {
     cinfo.input_components = 1;                /* # of color components per pixel */
     cinfo.in_color_space = JCS_GRAYSCALE;      /* colorspace of input image */
   }
@@ -399,11 +625,43 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
   jpeg_set_defaults(&cinfo);
   jpeg_set_quality(&cinfo, quality, TRUE);  /* limit to baseline-JPEG values */
 
+  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) {
+  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) {
@@ -416,22 +674,26 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
     }
   }
   else {
-    unsigned char *data = mymalloc(im->xsize * im->channels);
+    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(im, 0, im->xsize, cinfo.next_scanline, data, 
-                NULL, im->channels);
+        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_finish_compress(&cinfo);
       jpeg_destroy_compress(&cinfo);
+      i_push_error(0, "out of memory");
       return 0; /* out of memory? */
     }
   }
@@ -442,5 +704,21 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
 
   jpeg_destroy_compress(&cinfo);
 
+  ig->closecb(ig);
+
   return(1);
 }
+
+/*
+=back
+
+=head1 AUTHOR
+
+Arnar M. Hrafnkelsson, addi@umich.edu
+
+=head1 SEE ALSO
+
+Imager(3)
+
+=cut
+*/