From 6e4af7d46933dd3b56d496798a4de860e19e7cd9 Mon Sep 17 00:00:00 2001 From: Tony Cook Date: Tue, 1 Apr 2008 06:47:28 +0000 Subject: [PATCH] - 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 --- Changes | 5 ++ Imager.pm | 10 ++++ image.c | 20 ++++++++ imager.h | 1 + jpeg.c | 45 ++++++++++++++--- lib/Imager/Files.pod | 5 +- lib/Imager/ImageTypes.pod | 9 ++++ lib/Imager/Test.pm | 34 ++++++++++++- paste.im | 101 ++++++++++++++++++++++++++++++++++++++ t/t101jpeg.t | 32 +++++++++--- 10 files changed, 246 insertions(+), 16 deletions(-) diff --git a/Changes b/Changes index 26cc877f..f9086cff 100644 --- 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 diff --git a/Imager.pm b/Imager.pm index b00cdf26..29cb1872 100644 --- 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 110f5ef1..79230fc5 100644 --- 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 diff --git a/imager.h b/imager.h index d0b19234..b9abc818 100644 --- 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 f3d5fc7f..38566f51 100644 --- 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 */ diff --git a/lib/Imager/Files.pod b/lib/Imager/Files.pod index 288fd098..b22a0e20 100644 --- a/lib/Imager/Files.pod +++ b/lib/Imager/Files.pod @@ -433,8 +433,9 @@ image when this tag is non-zero. =head2 JPEG You can supply a C 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 parameter (or tag). $img->write(file=>'foo.jpg', jpegquality=>90) or die $img->errstr; diff --git a/lib/Imager/ImageTypes.pod b/lib/Imager/ImageTypes.pod index 71a0f08c..a64f5632 100644 --- a/lib/Imager/ImageTypes.pod +++ b/lib/Imager/ImageTypes.pod @@ -824,6 +824,15 @@ whether an image is worth processing. XXi_format - The file format this file was read from. +=item * + +XXi_background - used when writing +an image with an alpha channel to a file format that doesn't support +alpha channels. The C 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 +CIC<,>IC<,>IC<)>, eg color(255,0,0). + =back =head2 Quantization options diff --git a/lib/Imager/Test.pm b/lib/Imager/Test.pm index 994ccc43..a8300709 100644 --- a/lib/Imager/Test.pm +++ b/lib/Imager/Test.pm @@ -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(<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 diff --git a/t/t101jpeg.t b/t/t101jpeg.t index dd60d9fb..5279f132 100644 --- a/t/t101jpeg.t +++ b/t/t101jpeg.t @@ -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 -- 2.30.2