1 package BSE::Thumb::Imager;
3 #use blib '/home/tony/dev/imager/maint/Imager/';
6 my ($class, $cfg) = @_;
8 return bless { cfg => $cfg }, $class;
12 my ($geometry, $error) = @_;
14 my %geo = ( action => 'scale' );
15 if ($geometry =~ s/^(\d+)x(\d+)//) {
16 @geo{qw(width height)} = ( $1, $2 );
18 elsif ($geometry =~ s/^x(\d+)//) {
21 elsif ($geometry =~ s/^(\d+)x//) {
24 elsif ($geometry =~ s/^(\d+)//) {
25 $geo{width} = $geo{height} = $1;
28 $$error = "No leading dimension";
32 if ($geometry =~ s/^,?c//) {
34 if (!$geo{width} || !$geo{height}) {
35 $$error = "Both dimensions much be supplied for crop";
39 if ($geometry =~ s/^,fill:([^,]+)//) {
41 if (!$geo{width} || !$geo{height}) {
42 $$error = "scale:Both dimensions must be supplied for fill";
50 sub _parse_roundcorners {
51 my ($text, $error) = @_;
55 action => 'roundcorners',
64 if ($text =~ s/^radi(?:us|i):([\d ]+),?//) {
65 my @radii = split ' ', $1;
67 $geo{tl} = $geo{tr} = $geo{br} = $geo{bl} = $radii[0];
70 $geo{tl} = $geo{tr} = $radii[0];
71 $geo{bl} = $geo{br} = $radii[1];
74 @geo{qw/tl tr bl br/} = @radii;
77 $$error = 'roundcorners(radius:...) only accepts 1,2,4 radii';
81 if ($text =~ s/^bg:([^,]+),?//) {
84 if ($text =~ s/^bgalpha:(\d+)//) {
88 $$error = "unexpected junk in roundcorners: $text";
96 my ($text, $error) = @_;
109 perspectiveangle => '0',
112 while ($text =~ s/^(\w+):([^,]+),?//) {
113 unless (exists $mirror{$1}) {
114 $$error = "Unknown mirror parameter $1";
122 $$error = "unexpected junk in mirror: $text";
130 my ($text, $error) = @_;
137 if ($text =~ s/^([^,]+),?//) {
141 $$error = "unexpected junk in mirror: $text";
148 my ($text, $error) = @_;
150 unless ($text =~ s/^(\w+),?//) {
151 $$error = "filter: no filter name";
159 while ($text =~ s/^(\w+):([^,]+),?//) {
166 sub _parse_geometry {
167 my ($geometry, $error) = @_;
170 while (length $geometry) {
171 if ($geometry =~ s/^scale\(([^\)]+)\)//) {
172 my $scale = _parse_scale($1, $error)
176 elsif ($geometry =~ s/^roundcorners\(([^\)]*)\)//) {
177 my $round = _parse_roundcorners($1, $error)
181 elsif ($geometry =~ s/^mirror\(([^\)]*)\)//) {
182 my $mirror = _parse_mirror($1, $error)
186 elsif ($geometry =~ s/^grey\(\)//) {
187 push @geo, { action => 'grey' };
189 elsif ($geometry =~ s/^sepia\(([^\)]*)\)//) {
190 my $sepia = _parse_sepia($1, $error)
194 elsif ($geometry =~ s/^filter\(([^\)]*)\)//) {
195 my $filter = _parse_filter($1, $error)
200 $$error = "Unexpected junk at the end of the geometry $geometry";
209 sub validate_geometry {
210 my ($self, $geometry, $rerror) = @_;
212 _parse_geometry($geometry, $rerror)
218 sub thumb_dimensions_sized {
219 my ($self, $geometry, $width, $height) = @_;
222 my $geolist = _parse_geometry($geometry, \$error)
227 for my $geo (@$geolist) {
228 if ($geo->{action} eq 'scale') {
230 # fill always produces an image the right size
231 ($width, $height) = @$geo{qw/width height/};
236 my $width_scale = $geo->{width} / $width;
237 my $height_scale = $geo->{height} / $height;
238 my $scale = $width_scale < $height_scale ? $height_scale : $width_scale;
242 $width > $geo->{width} and $width = $geo->{width};
243 $height > $geo->{height} and $height = $geo->{height};
246 if ($geo->{width} && $width > $geo->{width}) {
247 $height = $height * $geo->{width} / $width;
248 $width = $geo->{width};
250 if ($geo->{height} && $height > $geo->{height}) {
251 $width = $width * $geo->{height} / $height;
252 $height = $geo->{height};
255 $width = int($width);
256 $height = int($height);
258 elsif ($geo->{action} eq 'roundcorners') {
259 if ($geo->{bgalpha} != 255) {
263 elsif ($geo->{action} eq 'mirror') {
264 $height += _percent_of($geo->{height}, $height)
265 + _percent_of($geo->{horizon}, $height);
266 $height = int($height);
268 if ($geo->{bgalpha} != 255) {
274 return ($width, $height, $req_alpha);
278 $_[0] < $_[1] ? $_[0] : $_[1];
282 $_[0] > $_[1] ? $_[0] : $_[1];
285 sub _do_roundcorners {
286 my ($src, $geo) = @_;
288 if ($src->getchannels == 1 || $src->getchannels == 2) {
289 $src = $src->convert(preset => 'rgb');
292 my $bg = _bgcolor($geo);
293 my $channels = $src->getchannels;
294 if ($geo->{bgalpha} != 255 && $channels != 4) {
296 $src = $src->convert(preset => 'addalpha');
298 my $width = $src->getwidth;
299 my $height = $src->getheight;
300 my $out = Imager->new(xsize => $width, ysize => $height,
301 channels => $channels);
303 require Imager::Fill;
304 my $fill = Imager::Fill->new(image => $src);
308 $out->box(filled => 1, color => $bg, xmax => $geo->{tl}-1, ymax => $geo->{tl}-1);
309 $out->arc(aa => 1, fill => $fill, r => $geo->{tl},
310 x => $geo->{tl}, y => $geo->{tl},
311 d1 => 175, d2 => 275);
314 $out->box(filled => 1, color => $bg, xmin => $width - $geo->{tr}, ymax => $geo->{tr}-1);
315 $out->arc(aa => 1, fill => $fill, r => $geo->{tr},
316 x => $width - $geo->{tr}, y => $geo->{tr},
317 d1 => 265, d2 => 365);
320 $out->box(filled => 1, color => $bg, xmax => $geo->{bl}-1,
321 ymin => $height - $geo->{bl});
322 $out->arc(aa => 1, fill => $fill, r => $geo->{bl},
323 x => $geo->{bl}, y => $height - $geo->{bl},
324 d1 => 85, d2 => 185);
327 $out->box(filled => 1, color => $bg, xmin => $width - $geo->{br},
328 ymin => $height - $geo->{br});
329 $out->arc(aa => 1, fill => $fill, r => $geo->{br},
330 x => $width - $geo->{br}, y => $height - $geo->{br},
331 d1 => 355, d2 => 455);
338 my ($work, $mirror) = @_;
340 if ($work->getchannels < 3) {
341 $work = $work->convert(preset => 'rgb');
343 if ($mirror->{bgalpha} != 255) {
344 $work = $work->convert(preset => 'addalpha');
347 my $bg = _bgcolor($mirror);
349 my $oldheight = $work->getheight();
350 my $height = $oldheight;
351 my $gap = int(_percent_of($mirror->{horizon}, $oldheight));
352 my $add_height = int(_percent_of($mirror->{height}, $oldheight));
353 $height += $gap + $add_height;
355 my $out = Imager->new(xsize => $work->getwidth, ysize => $height,
356 channels => $work->getchannels);
357 $out->box(filled => 1, color => $bg);
359 if ($work->getchannels == 4) {
360 $out->rubthrough(src => $work)
361 or print STDERR $out->errstr, "\n";
364 $out->paste(src => $work)
365 or print STDERR $out->errstr, "\n";
368 $work->flip(dir => 'v');
370 $work = $work->crop(bottom => $add_height);
372 $work = Imager::transform2
378 opacity => _percent($mirror->{opacity})
381 $mirror->{srcx} !srcx
382 $mirror->{srcy} !srcy
383 \@srcx \@srcy getp1 !p
384 \@p red \@p green \@p blue
385 \@p alpha opacity h y - h / * * rgba
389 ) or die Imager->errstr;
390 $out->rubthrough(src => $work, ty => $oldheight + $gap);
392 if ($mirror->{perspective}) {
393 require Imager::Matrix2d;
394 my $old_width = $out->getwidth;
395 my $p = abs($mirror->{perspective});
396 my $new_width = $old_width / (1 + $p * $old_width) + 1;
397 my $angle = sin($mirror->{perspectiveangle} * 3.1415926 / 180);
398 my $persp = bless [ 1, 0, 0,
400 -abs($p), 0, 1 ], 'Imager::Matrix2d';
401 $out->flip(dir => 'v');
402 $mirror->{perspective} < 0 and $out->flip(dir => 'h');
403 my $temp = $out->matrix_transform(matrix=> $persp, back=>$bg, xsize => $new_width)
404 or print STDERR "failed", $work->errstr, "\n";
406 $mirror->{perspective} < 0 and $out->flip(dir => 'h');
407 $out->flip(dir => 'v');
415 my ($work, $sepia) = @_;
417 require Imager::Filter::Sepia;
418 $work = $work->convert(preset => 'rgb')
419 if $work->getchannels < 3;
420 my $color = Imager::Color->new($sepia->{color});
421 $work->filter(type => 'sepia', tone => $color);
427 my ($work, $filter) = @_;
429 $work = $work->convert(preset => 'rgb')
430 if $work->getchannels < 3;
432 $work->filter(%$filter)
433 or die $work->errstr;
439 my ($self, $filename, $geometry, $error) = @_;
441 my $geolist = _parse_geometry($geometry, $error)
445 my $src = Imager->new;
446 unless ($src->read(file => $filename)) {
447 $$error = "Cannot read image $filename: ".$src->errstr;
452 for my $geo (@$geolist) {
453 if ($geo->{action} eq 'scale') {
456 my $width_scale = $geo->{width} / $work->getwidth;
457 my $height_scale = $geo->{height} / $work->getheight;
458 my $scale = $width_scale < $height_scale ? $height_scale : $width_scale;
460 $scaled = $work->scale(scalefactor => $scale, qtype=>'mixing');
462 my $width = $scaled->getwidth;
463 if ($width > $geo->{width}) {
464 $scaled = $scaled->crop(left => ($width-$geo->{width})/2, width => $geo->{width});
466 my $height = $scaled->getheight;
467 if ($height > $geo->{height}) {
468 $scaled = $scaled->crop(top => ($height - $geo->{height}) / 2,
469 height => $geo->{height});
473 my $width = $work->getwidth;
474 my $height = $work->getheight;
475 if ($geo->{width} && $width > $geo->{width}) {
476 $height = $height * $geo->{width} / $width;
477 $width = $geo->{width};
479 if ($geo->{height} && $height > $geo->{height}) {
480 $width = $width * $geo->{height} / $height;
481 $height = $geo->{height};
483 $width = int($width);
484 $height = int($height);
485 $scaled = $work->scale(xpixels => $width, ypixels => $height,
489 my $result = $scaled;
491 ($scaled->getwidth < $geo->{width} || $scaled->getheight < $geo->{height})) {
492 $result = Imager->new(xsize => $geo->{width}, ysize => $geo->{height});
493 $result->box(color => $geo->{fill}, filled => 1);
494 $result->paste(left => ($geo->{width} - $scaled->getwidth) / 2,
495 top => ($geo->{height} - $scaled->getheight) / 2 ,
500 elsif ($geo->{action} eq 'roundcorners') {
501 $work = _do_roundcorners($work, $geo);
503 elsif ($geo->{action} eq 'mirror') {
504 $work = _do_mirror($work, $geo);
506 elsif ($geo->{action} eq 'grey') {
507 $work = $work->convert(preset => 'grey');
508 print STDERR "channels ", $work->getchannels, "\n";
510 elsif ($geo->{action} eq 'sepia') {
511 $work = _do_sepia($work, $geo);
513 elsif ($geo->{action} eq 'filter') {
514 $work = _do_filter($work, $geo);
519 my $type = $src->tags(name => 'i_format');
521 if ($work->getchannels == 4 || $work->getchannels == 2) {
525 unless ($work->write(data => \$data, type => $type)) {
526 $$error = "cannot write image ".$work->errstr;
530 return ( $data, "image/$type" );
536 if ($num =~ s/%$//) {
543 my ($num, $base) = @_;
545 if ($num =~ s/%$//) {
546 return $base * $num / 100.0;
556 my $bg = Imager::Color->new($spec->{bg});
557 $bg->set(($bg->rgba)[0..2], $spec->{bgalpha});