CMYK jpeg images were being read as 4 channel images, even though they
authorTony Cook <tony@develop=help.com>
Fri, 14 Jul 2006 14:57:44 +0000 (14:57 +0000)
committerTony Cook <tony@develop=help.com>
Fri, 14 Jul 2006 14:57:44 +0000 (14:57 +0000)
have no alpha channel.

The colors were being transferred directly from the JPEG image data,
this only looked correct because of an old bug in photoshop, kept for
compatibility in later versions.  Photoshop inverts the ink values in
the file, so max cyan coverage is stored at 0, and min as 255, and so
on.

CMYK jpegs are now read as 3 channel images.  The colors are now
converted adjusting for the photoshop bug, cmyk images from the only
other source I have, corel draw 9, are inverted in the same way,
presumably for compatibility with photoshop.

If anyone has an application that produces technically correct CMYK
jpegs, please provide a sample in jpeg and tiff form so I can attempt
to deal with it.

Fixes: http://rt.cpan.org/Ticket/Display.html?id=20416

MANIFEST
jpeg.c
t/t101jpeg.t
testimg/scmyk.jpg [new file with mode: 0644]

index cd4a635..6cfee79 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -294,7 +294,8 @@ testimg/penguin-base.ppm
 testimg/scale.gif
 testimg/scale.ppm
 testimg/scalei.gif
-testimg/scmyk.tif      Simple CMYK image
+testimg/scmyk.tif      Simple CMYK TIFF image
+testimg/scmyk.jpg      Simple CMYK JPEG image
 testimg/scmyka.tif     CMYK with one alpha channel
 testimg/scmykaa.tif    CMYK with 2 alpha channels
 testimg/screen2.gif
diff --git a/jpeg.c b/jpeg.c
index 3a78967..ab32e3d 100644 (file)
--- a/jpeg.c
+++ b/jpeg.c
@@ -337,6 +337,45 @@ my_error_exit (j_common_ptr cinfo) {
   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)
 
@@ -348,12 +387,14 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
 #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;
 
   mm_log((1,"i_readjpeg_wiol(data 0x%p, length %d,iptc_itext 0x%p)\n", data, length, iptc_itext));
 
@@ -369,6 +410,8 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
     jpeg_destroy_decompress(&cinfo); 
     *iptc_itext=NULL;
     *itlength=0;
+    if (line_buffer)
+      myfree(line_buffer);
     return NULL;
   }
   
@@ -380,14 +423,53 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
 
   (void) jpeg_read_header(&cinfo, TRUE);
   (void) jpeg_start_decompress(&cinfo);
+
+  channels = cinfo.output_components;
+  switch (cinfo.out_color_space) {
+  case JCS_GRAYSCALE:
+    transfer_f = transfer_gray;
+    break;
+  
+  case JCS_RGB:
+    transfer_f = transfer_rgb;
+    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,
-                                    cinfo.output_components, sizeof(i_sample_t))) {
+                                    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,cinfo.output_components);
+
+  im=i_img_empty_ch(NULL, cinfo.output_width, cinfo.output_height, channels);
   if (!im) {
     wiol_term_source(&cinfo);
     jpeg_destroy_decompress(&cinfo);
@@ -395,10 +477,13 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
   }
   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;
@@ -416,6 +501,9 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
     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;
@@ -573,6 +661,7 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
         row_pointer[0] = data;
         (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
       }
+      myfree(data);
     }
     else {
       jpeg_destroy_compress(&cinfo);
index 91f4bc4..74c4885 100644 (file)
@@ -2,7 +2,7 @@
 use strict;
 use lib 't';
 use Imager qw(:all);
-use Test::More tests => 71;
+use Test::More tests => 86;
 
 init_log("testout/t101jpeg.log",1);
 
@@ -30,7 +30,7 @@ if (!i_has_format("jpeg")) {
     $im = Imager->new(xsize=>2, ysize=>2);
     ok(!$im->write(file=>"testout/nojpeg.jpg"), "should fail to write jpeg");
     cmp_ok($im->errstr, '=~', qr/format not supported/, "check no jpeg message");
-    skip("no jpeg support", 67);
+    skip("no jpeg support", 82);
   }
 } else {
   open(FH,">testout/t101.jpg") || die "cannot open testout/t101.jpg for writing\n";
@@ -374,5 +374,34 @@ if (!i_has_format("jpeg")) {
     is($iptc{headln}, 'Dummy Headline!', 'check iptc headln');
     is($iptc{credit}, 'No Credit Given', 'check iptc credit');
   }
+
+  { # handling of CMYK jpeg
+    # http://rt.cpan.org/Ticket/Display.html?id=20416
+    my $im = Imager->new;
+    ok($im->read(file => 'testimg/scmyk.jpg'), 'read a CMYK jpeg');
+    is($im->getchannels, 3, "check channel count");
+    my $col = $im->getpixel(x => 0, 'y' => 0);
+    ok($col, "got the 'black' pixel");
+    # this is jpeg, so we can't compare colors exactly
+    # older versions returned this pixel at a light color, but
+    # it's black in the image
+    my ($r, $g, $b) = $col->rgba;
+    cmp_ok($r, '<', 10, 'black - red low');
+    cmp_ok($g, '<', 10, 'black - green low');
+    cmp_ok($b, '<', 10, 'black - blue low');
+    $col = $im->getpixel(x => 15, 'y' => 0);
+    ok($col, "got the dark blue");
+    ($r, $g, $b) = $col->rgba;
+    cmp_ok($r, '<', 10, 'dark blue - red low');
+    cmp_ok($g, '<', 10, 'dark blue - green low');
+    cmp_ok($b, '>', 110, 'dark blue - blue middle (bottom)');
+    cmp_ok($b, '<', 130, 'dark blue - blue middle (top)');
+    $col = $im->getpixel(x => 0, 'y' => 15);
+    ok($col, "got the red");
+    ($r, $g, $b) = $col->rgba;
+    cmp_ok($r, '>', 245, 'red - red high');
+    cmp_ok($g, '<', 10, 'red - green low');
+    cmp_ok($b, '<', 10, 'red - blue low');
+  }
 }
 
diff --git a/testimg/scmyk.jpg b/testimg/scmyk.jpg
new file mode 100644 (file)
index 0000000..3886a7a
Binary files /dev/null and b/testimg/scmyk.jpg differ