- writing a 2 or 4 channel image to a JPEG will now write that image as
authorTony Cook <tony@develop=help.com>
Tue, 1 Apr 2008 06:47:28 +0000 (06:47 +0000)
committerTony Cook <tony@develop=help.com>
Tue, 1 Apr 2008 06:47:28 +0000 (06:47 +0000)
   if composited against a background, black by default, overridable
   with the i_background tag/parameter.
   https://rt.cpan.org/Ticket/Display.html?id=29876

Changes
Imager.pm
image.c
imager.h
jpeg.c
lib/Imager/Files.pod
lib/Imager/ImageTypes.pod
lib/Imager/Test.pm
paste.im
t/t101jpeg.t

diff --git a/Changes b/Changes
index 26cc877..f9086cf 100644 (file)
--- a/Changes
+++ b/Changes
@@ -41,6 +41,11 @@ Imager 0.63 - unreleased
    bugzilla.
    http://rt.cpan.org/Ticket/Display.html?id=32926
 
+ - writing a 2 or 4 channel image to a JPEG will now write that image as
+   if composited against a background, black by default, overridable
+   with the i_background tag/parameter.
+   https://rt.cpan.org/Ticket/Display.html?id=29876
+
 Bug fixes:
 
  - Imager::Matrix2d->translate() now only requires one of the x or y
index b00cdf2..29cb187 100644 (file)
--- a/Imager.pm
+++ b/Imager.pm
@@ -1593,6 +1593,9 @@ my %obsolete_opts =
    gif_loop_count => 'gif_loop',
   );
 
+# options that should be converted to colors
+my %color_opts = map { $_ => 1 } qw/i_background/;
+
 sub _set_opts {
   my ($self, $opts, $prefix, @imgs) = @_;
 
@@ -1613,6 +1616,13 @@ sub _set_opts {
     }
     next unless $tagname =~ /^\Q$prefix/;
     my $value = $opts->{$opt};
+    if ($color_opts{$opt}) {
+      $value = _color($value);
+      unless ($value) {
+       $self->_set_error($Imager::ERRSTR);
+       return;
+      }
+    }
     if (ref $value) {
       if (UNIVERSAL::isa($value, "Imager::Color")) {
         my $tag = sprintf("color(%d,%d,%d,%d)", $value->rgba);
diff --git a/image.c b/image.c
index 110f5ef..79230fc 100644 (file)
--- a/image.c
+++ b/image.c
@@ -2433,6 +2433,26 @@ i_img_is_monochrome(i_img *im, int *zero_is_white) {
   return 0;
 }
 
+/*
+=item i_get_file_background(im, &bg)
+
+Retrieve the file write background color tag from the image.
+
+If not present, returns black.
+
+=cut
+*/
+
+void
+i_get_file_background(i_img *im, i_color *bg) {
+  if (!i_tags_get_color(&im->tags, "i_background", 0, bg)) {
+    /* black default */
+    bg->channel[0] = bg->channel[1] = bg->channel[2] = 0;
+  }
+  /* always full alpha */
+  bg->channel[3] = 255;
+}
+
 /*
 =back
 
index d0b1923..b9abc81 100644 (file)
--- a/imager.h
+++ b/imager.h
@@ -369,6 +369,7 @@ extern i_img *i_img_to_rgb16(i_img *im);
 extern i_img *i_img_double_new(int x, int y, int ch);
 
 extern int i_img_is_monochrome(i_img *im, int *zero_is_white);
+extern void i_get_file_background(i_img *im, i_color *bg);
 
 const char * i_test_format_probe(io_glue *data, int length);
 
diff --git a/jpeg.c b/jpeg.c
index f3d5fc7..38566f5 100644 (file)
--- a/jpeg.c
+++ b/jpeg.c
@@ -572,6 +572,7 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
   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 my_error_mgr jerr;
@@ -579,6 +580,7 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
   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));
   
@@ -586,8 +588,7 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
   io_glue_commit_types(ig);
 
   if (!(im->channels==1 || im->channels==3)) { 
-    i_push_error(0, "only 1 or 3 channels images can be saved as JPEG");
-    return 0;
+    want_channels = im->channels - 1;
   }
   quality = qfactor;
 
@@ -601,6 +602,8 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
     jpeg_destroy_compress(&cinfo);
     if (data)
       myfree(data);
+    if (line_buf)
+      myfree(line_buf);
     return 0;
   }
 
@@ -609,12 +612,12 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
   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 */
   }
@@ -657,7 +660,8 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
 
   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) {
@@ -669,7 +673,7 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
       (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
     }
   }
-  else {
+  else if (im->channels == want_channels) {
     data = mymalloc(im->xsize * im->channels);
     if (data) {
       while (cinfo.next_scanline < cinfo.image_height) {
@@ -690,6 +694,35 @@ i_writejpeg_wiol(i_img *im, io_glue *ig, int qfactor) {
       return 0; /* out of memory? */
     }
   }
+  else {
+    i_color bg;
+    int x;
+    int ch;
+    i_color const *linep;
+    unsigned char * datap;    
+    
+    line_buf = mymalloc(sizeof(i_color) * im->xsize);
+
+    i_get_file_background(im, &bg);
+
+    data = mymalloc(im->xsize * want_channels);
+    while (cinfo.next_scanline < cinfo.image_height) {
+      i_glin(im, 0, im->xsize, cinfo.next_scanline, line_buf);
+      i_adapt_colors_bg(want_channels, im->channels, line_buf, im->xsize, &bg);
+      datap = data;
+      linep = line_buf;
+      for (x = 0; x < im->xsize; ++x) {
+       for (ch = 0; ch < want_channels; ++ch) {
+         *datap++ = linep->channel[ch];
+       }
+       ++linep;
+      }
+      row_pointer[0] = data;
+      (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
+    }
+    myfree(line_buf);
+    myfree(data);
+  }
 
   /* Step 6: Finish compression */
 
index 288fd09..b22a0e2 100644 (file)
@@ -433,8 +433,9 @@ image when this tag is non-zero.
 =head2 JPEG
 
 You can supply a C<jpegquality> parameter (0-100) when writing a JPEG
-file, which defaults to 75%.  Only 1 and 3 channel images
-can be written, including 1 and 3 channel paletted images.
+file, which defaults to 75%.  If you write an image with an alpha
+channel to a jpeg file then it will be composited against the
+background set by the C<i_background> parameter (or tag).
 
   $img->write(file=>'foo.jpg', jpegquality=>90) or die $img->errstr;
 
index 71a0f08..a64f563 100644 (file)
@@ -824,6 +824,15 @@ whether an image is worth processing.
 X<i_format tag>X<tags, i_format>i_format - The file format this file
 was read from.
 
+=item *
+
+X<i_background>X<tags, i_background>i_background - used when writing
+an image with an alpha channel to a file format that doesn't support
+alpha channels.  The C<write> method will convert a normal color
+specification like "#FF0000" into a color object for you, but if you
+set this as a tag you will need to format it like
+C<color(>I<red>C<,>I<green>C<,>I<blue>C<)>, eg color(255,0,0).
+
 =back
 
 =head2 Quantization options
index 994ccc4..a830070 100644 (file)
@@ -6,7 +6,7 @@ use vars qw(@ISA @EXPORT_OK);
 @ISA = qw(Exporter);
 @EXPORT_OK = qw(diff_text_with_nul 
                 test_image_raw test_image_16 test_image test_image_double 
-                is_color3 is_color1 is_color4 
+                is_color3 is_color1 is_color4 is_color_close3
                 is_fcolor4
                 is_image is_image_similar 
                 image_bounds_checks mask_tests
@@ -61,6 +61,38 @@ END_DIAG
   return 1;
 }
 
+sub is_color_close3($$$$$$) {
+  my ($color, $red, $green, $blue, $tolerance, $comment) = @_;
+
+  my $builder = Test::Builder->new;
+
+  unless (defined $color) {
+    $builder->ok(0, $comment);
+    $builder->diag("color is undef");
+    return;
+  }
+  unless ($color->can('rgba')) {
+    $builder->ok(0, $comment);
+    $builder->diag("color is not a color object");
+    return;
+  }
+
+  my ($cr, $cg, $cb) = $color->rgba;
+  unless ($builder->ok(abs($cr - $red) <= $tolerance
+                      && abs($cg - $green) <= $tolerance
+                      && abs($cb - $blue) <= $tolerance, $comment)) {
+    $builder->diag(<<END_DIAG);
+Color out of tolerance ($tolerance):
+  Red: expected $red vs received $cr
+Green: expected $green vs received $cg
+ Blue: expected $blue vs received $cb
+END_DIAG
+    return;
+  }
+
+  return 1;
+}
+
 sub is_color4($$$$$$) {
   my ($color, $red, $green, $blue, $alpha, $comment) = @_;
 
index f162359..45305df 100644 (file)
--- a/paste.im
+++ b/paste.im
@@ -228,5 +228,106 @@ i_adapt_fcolors
   }
 }
 
+void
+#ifdef IM_EIGHT_BIT
+i_adapt_colors_bg
+#else
+i_adapt_fcolors_bg
+#endif
+(int out_channels, int in_channels, IM_COLOR *colors, 
+              size_t count, IM_COLOR const *bg) {
+  if (out_channels == in_channels)
+    return;
+  if (count == 0)
+    return;
+
+  switch (out_channels) {
+  case 2:
+  case 4:
+    IM_ADAPT_COLORS(out_channels, in_channels, colors, count);
+    return;
+
+  case 1:
+    switch (in_channels) {
+    case 3:
+      IM_ADAPT_COLORS(out_channels, in_channels, colors, count);
+      return;
+
+    case 2:
+      {
+       /* apply alpha against our given background */
+       IM_WORK_T grey_bg = IM_ROUND(color_to_grey(bg));
+       while (count) {
+         colors->channel[0] = 
+           (colors->channel[0] * colors->channel[1] +
+            grey_bg * (IM_SAMPLE_MAX - colors->channel[1])) / IM_SAMPLE_MAX;
+         ++colors;
+         --count;
+       }
+      }
+      break;
+
+    case 4:
+      {
+       IM_WORK_T grey_bg = IM_ROUND(color_to_grey(bg));
+       while (count) {
+         IM_WORK_T src_grey = IM_ROUND(color_to_grey(colors));
+         colors->channel[0] =
+           (src_grey * colors->channel[3]
+            + grey_bg * (IM_SAMPLE_MAX - colors->channel[3])) / IM_SAMPLE_MAX;
+         ++colors;
+         --count;
+       }
+      }
+      break;
+    }
+    break;
+      
+  case 3:
+    switch (in_channels) {
+    case 1:
+      IM_ADAPT_COLORS(out_channels, in_channels, colors, count);
+      return;
+
+    case 2:
+      {
+       while (count) {
+         int ch;
+         IM_WORK_T src_grey = colors->channel[0];
+         IM_WORK_T src_alpha = colors->channel[1];
+         for (ch = 0; ch < 3; ++ch) {
+           colors->channel[ch] =
+             (src_grey * src_alpha
+              + bg->channel[ch] * (IM_SAMPLE_MAX - src_alpha)) 
+             / IM_SAMPLE_MAX;
+         }
+         ++colors;
+         --count;
+       }
+      }
+      break;
+
+    case 4:
+      {
+       while (count) {
+         int ch;
+         IM_WORK_T src_alpha = colors->channel[3];
+         for (ch = 0; ch < 3; ++ch) {
+           colors->channel[ch] =
+             (colors->channel[ch] * src_alpha
+              + bg->channel[ch] * (IM_SAMPLE_MAX - src_alpha)) 
+             / IM_SAMPLE_MAX;
+         }
+         ++colors;
+         --count;
+       }
+      }
+      break;
+    }
+    break;
+  }
+
+}
+
 #/code
 
index dd60d9f..5279f13 100644 (file)
@@ -1,7 +1,8 @@
 #!perl -w
 use strict;
 use Imager qw(:all);
-use Test::More tests => 88;
+use Test::More tests => 94;
+use Imager::Test qw(is_color_close3);
 
 init_log("testout/t101jpeg.log",1);
 
@@ -31,7 +32,7 @@ if (!i_has_format("jpeg")) {
     cmp_ok($im->errstr, '=~', qr/format 'jpeg' not supported/, "check no jpeg message");
     ok(!grep($_ eq 'jpeg', Imager->read_types), "check jpeg not in read types");
     ok(!grep($_ eq 'jpeg', Imager->write_types), "check jpeg not in write types");
-    skip("no jpeg support", 82);
+    skip("no jpeg support", 88);
   }
 } else {
   open(FH,">testout/t101.jpg") || die "cannot open testout/t101.jpg for writing\n";
@@ -265,12 +266,29 @@ if (!i_has_format("jpeg")) {
     # attempting to write a 4 channel image to a bufchain would
     # cause a seg fault.
     # it should fail still
-    my $im = Imager->new(xsize => 10, ysize => 10, channels => 4);
+    # overridden by # 29876
+    # give 4/2 channel images a background color when saving to JPEG
+    my $im = Imager->new(xsize => 16, ysize => 16, channels => 4);
+    $im->box(filled => 1, xmin => 8, color => '#FFE0C0');
     my $data;
-    ok(!$im->write(data => \$data, type => 'jpeg'),
-       "should fail to write but shouldn't crash");
-    is($im->errstr, "only 1 or 3 channels images can be saved as JPEG",
-       "check the error message");
+    ok($im->write(data => \$data, type => 'jpeg'),
+       "should write with a black background");
+    my $imread = Imager->new;
+    ok($imread->read(data => $data, type => 'jpeg'), 'read it back');
+    is_color_close3($imread->getpixel('x' => 0, 'y' => 0), 0, 0, 0, 4,
+             "check it's black");
+    is_color_close3($imread->getpixel('x' => 15, 'y' => 9), 255, 224, 192, 4,
+                   "check filled area filled");
+
+    # write with a red background
+    $data = '';
+    ok($im->write(data => \$data, type => 'jpeg', i_background => '#FF0000'),
+       "write with red background");
+    ok($imread->read(data => $data, type => 'jpeg'), "read it back");
+    is_color_close3($imread->getpixel('x' => 0, 'y' => 0), 255, 0, 0, 4,
+             "check it's red");
+    is_color_close3($imread->getpixel('x' => 15, 'y' => 9), 255, 224, 192, 4,
+                   "check filled area filled");
   }
  SKIP:
   { # Issue # 18496