#!/usr/bin/perl -w my $__V = <<'V'; # $Id: fotofix,v 1.67 2008/06/12 21:00:12 dk Exp $ V # simple image viewer with simple capabilities to take care of # freshly downloaded photos from your camera - can walk image lists, # rotate images, and remove red eyes (with some luck, and if IPA is installed) # # dependencies: # Prima: http://prima.eu.org/anon_cvs.html ( but will also work with # http://search.cpan.org/CPAN/authors/id/K/KA/KARASIK/Prima-1.22.tar.gz ) # # optional dependencies: # IPA: http://search.cpan.org/CPAN/authors/id/K/KA/KARASIK/IPA-1.03.tar.gz # (or http://prima.eu.org/IPA/IPA-1.03.tar.gz if CPAN refuses) # # Prima::Image::Magick: # http://search.cpan.org/~karasik/Prima-Image-Magick-0.02 # use strict; use warnings; use Prima 1.26 qw( Application ImageViewer StdDlg MsgBox IniFile EventHook ComboBox ); use Cwd qw(getcwd abs_path); eval "use IPA;"; my $UseIPA = not $@; eval "use Prima::Image::Magick;"; my $UseImageMagick = not $@; $__V =~ /v ([\d.]+)/; my $VERSION = $1; # If on, can be (not necessarily) , faster but surely will cost extra memory. # Since X11 doesn't support scaled image output, this doesn't matter, and should be # always disabled. Win32 on the contrary does, but might (or might not) be slower # than Prima native image scaling. my $UseBufferedZoom = ( $::application-> get_system_info->{apc} == apc::Unix) ? 0 : 1; # used in fullscreen mode my $UseShapeExtension = $::application-> get_system_value( sv::ShapeExtension); # This is the only parameter the red eye detector cares about, and is a balance of # how much green and blue should be there to counterbalance the red. For example, # 1.0 (default) will remove these red spots where the intensity or red is more # than sum of intensities of blue and green. $HueDiff less than 1 makes the detector # to behave more aggressively, whereas $HueDiff more than 1 makes it be more strict about # what is to be considered the red hue. Values outside 0.8-2.0 are probably not # practically useful. my $RedEyesHueDiff = 1.0; my @MagnifyingGlassSize = (302,202); my ( $w, $menu, $iv, $image, $magnify, $magnify_zoom, $ini, $want_prebuffered_zoom, $use_prebuffered_zoom, $image_is_loading, %neighbour_files_cache, $filename, $filecodec, $region, %icons, @window_rect, $current_pixel, %tags, @max_window_size, $IPALoaded, $open_dialog, $save_dialog, $chdir_dialog, $slideshow, $animation, $codecs, ); my $zoom = 1.0; my @last_size = (0,0); my $fullscreen_x11 = $::application-> get_system_info->{apc} == apc::Unix; my $modified = 0; my $fullscreen = 0; my $conversion = ict::Optimized; my $conversion_menuid = 'P'; my %image_format_category = ( im::Color => '%06x', im::GrayScale => '%d', im::GrayScale|im::RealNumber => '%g', ); { $_=< create( width => 32, height => 32, type => im::BW, data => substr($_,0,128), mask => substr($_,128,128), ); create_pointer( $icons{hand}); } sub create_pointer { my $i = shift; my @p = ( $::application-> get_system_value( sv::XPointer), $::application-> get_system_value( sv::YPointer) ); return if $p[0] <= $i-> width or $p[1] <= $i-> height; # let Prima deal with funky sizes # here, we just enlarge the icon without stretching $i-> set( hScaling => 0, vScaling => 0, ); my ( $x, $a) = $i-> split; my $aa = $a-> dup; $_-> size( @p) for $x, $aa; $aa-> data( ~ $aa-> data); $aa-> put_image( 0, 0, $a); $i-> combine( $x, $aa); } sub loadIPA { return 1 if $IPALoaded; unless ( $UseIPA) { my $func = shift || 'This function'; message("$func requires IPA module installed"); return 0; } require IPA::Misc; IPA::Misc-> import('/./'); require IPA::Point; IPA::Point-> import('/./'); require IPA::Geometry; IPA::Geometry-> import('/./'); $IPALoaded++; return 1; } sub image_reset_display_buffer { $use_prebuffered_zoom = $want_prebuffered_zoom ? can_use_prebuffered_zoom() : 0; if ( $use_prebuffered_zoom) { my $g; if ( $ini-> {Scaling} ne '1') { $g = Prima::Image::Magick::prima_to_magick( $image); $g-> Resize( width => int($image-> width * $zoom), height => int($image-> height * $zoom), filter => $ini-> {Scaling}, ); $g = $g-> Prima; } else { $g = $image-> dup; $g-> size( int($image-> width * $zoom), int($image-> height * $zoom)); } $iv-> zoom( 1.0); $iv-> image( $g); } else { $iv-> image( $image); $iv-> zoom( $zoom); } $iv-> palette( $image-> palette); } sub image_replace { my $i = shift; $region = undef; magnify(0); animation_close(); my $eq = $image && $i && (join('x', $i-> size) eq join('x', $image-> size)); $image = $i; image_reset_display_buffer(); $modified = 1; update_window_title(); update_menu_status(); update_window_size() unless $eq; } sub can_use_prebuffered_zoom { return 0 if $ini-> {Scaling} eq '0'; return 0 if $zoom == 1.0; # duh return 0 if $image_is_loading; # no point in replotting expensive scaling on each image read return 1 if $ini-> {Scaling} ne '1'; my @as = $::application-> size; return 0 if $zoom * $image-> width > $as[0] or $zoom * $image-> height > $as[1]; 1; } sub zoom_set { my $old_zoom = $zoom; ($zoom, $want_prebuffered_zoom) = @_; $want_prebuffered_zoom = 1 unless defined $want_prebuffered_zoom; $zoom = 0.02 if $zoom < 0.02; $zoom = 100 if $zoom > 100; return if $zoom == $old_zoom; $magnify-> repaint if $magnify; image_reset_display_buffer(); update_window_title(); } # returns zoom factor required to fit the image to the given size sub zoom_from_window_size { my @xs = @_; my @is = $image-> size; my @as = $iv-> get_active_area(2, @xs); # expect scrollbars to disappear $as[0] += $iv-> VScroll-> width - 1 if $iv-> vScroll; $as[1] += $iv-> HScroll-> height - 1 if $iv-> hScroll; my $x = $as[0] / $is[0]; my $y = $as[1] / $is[1]; my $zoom = ($x < $y) ? $x : $y; $zoom = $iv-> zoom_round( $zoom); # Zoom roundoffs may create a slighlty larger zoom which might result # in (undesirable) scrollbars. We fight this by reducing zoom factor slightly. while ( grep { $xs[$_] < int($is[$_] * $zoom + .5)} (0,1)) { my $z = $iv-> zoom_round( $zoom - 0.01); last if $z >= $zoom; $zoom = $z; } return $zoom; } sub zoom_scale { zoom_set $zoom * shift } sub zoom_best_fit { zoom_set( zoom_from_window_size( $iv-> size), 1) if $image; } sub convert_screen_to_point { return $iv-> screen2point(@_) unless $use_prebuffered_zoom; my $ivzoom = $iv-> zoom; return map { $_ * $ivzoom / $zoom } $iv-> screen2point(@_); } sub convert_point_to_screen { return $iv-> point2screen(@_) unless $use_prebuffered_zoom; my $ivzoom = $iv-> zoom; return $iv-> point2screen( map { $_ * $zoom / $ivzoom } @_); } sub region_set { my @r = map { int } ( @_ ? @_ : (0,0,0,0)); return unless $image; my @s = $image-> size; @r[0,2] = @r[2,0] if $r[2] < $r[0]; @r[1,3] = @r[3,1] if $r[3] < $r[1]; for ( @r) { $_ = 0 if $_ < 0; } $r[0] = 0 if $r[0] < 0; $r[1] = 0 if $r[1] < 0; $r[2] = $s[0] - 1 if $r[2] >= $s[0]; $r[3] = $s[1] - 1 if $r[3] >= $s[1]; @r = (0,0,0,0) if $r[0] >= $s[0] or $r[1] >= $s[1] or $r[2] < 0 or $r[3] < 0 or ( $r[0] == $r[2] and $r[1] == $r[3]); my $r = $region; $region = ( grep { $_ != 0 } @r ) ? \@r : undef; return if not defined($r) and not defined($region); $iv-> repaint; } sub image_as_displayed { my $i = $iv-> image; if ( $use_prebuffered_zoom) { $i = $i-> extract( map { int( $zoom * $_ + 0.5 ) } $region->[0], $region->[1], $region->[2] - $region->[0], $region->[3] - $region->[1] ) if $region; } elsif ( $zoom != 1.0 or $region) { $i = $region ? $i-> extract( $region->[0], $region->[1], $region->[2] - $region->[0], $region->[3] - $region->[1] ) : $i-> dup; $i-> size( $i-> width * $zoom, $i-> height * $zoom) if $zoom != 1.0; } $i; } sub region_image { $region ? $image-> extract( $region->[0], $region->[1], $region->[2] - $region->[0], $region->[3] - $region->[1], ) : $image; } sub draw_marquee { my $o = $::application; $o-> begin_paint; $o-> rect_focus( $iv-> client_to_screen( convert_point_to_screen( @{$iv}{qw(x y marquee_x marquee_y)} ) ) ); $o-> end_paint; } # Try to get maximal window extensions. In case WM resizes us back, # record this, and adjust accordingly sub get_client_size { return @max_window_size if 2 == grep { defined } @max_window_size; my @as = $::application-> size; $as[0] -= $::application-> get_system_value(sv::XbsSizeable) * 2; $as[1] -= $::application-> get_system_value(sv::YbsSizeable) * 2 + $::application-> get_system_value(sv::YMenu) + $::application-> get_system_value(sv::YTitleBar); my @i = $::application-> get_indents(); $as[0] -= $i[0] + $i[2]; $as[1] -= $i[1] + $i[3]; for (0,1) { $as[$_] = $max_window_size[$_] if defined $max_window_size[$_]; } @as; } sub update_window_title { my $img = $image; my $str; if ( $img) { $str = defined($filename) ? $filename : '.Untitled'; $str =~ m/([^\\\/]*)$/; my $f = $1; if ( $slideshow) { my ( undef, $index, @files) = get_dir_list(); $str = sprintf("(%d/%d) %s", ($index||0) + 1, scalar(@files), $f); } else { $str = sprintf("%s (%dx%dx%d)", $f, $img-> width, $img-> height, $img-> type & im::BPP); } } else { $str = '.Untitled'; } if ( $iv-> {drag} and $iv->{drag} == mb::Left) { $str .= " [" . abs( $iv->{marquee_x} - $iv->{x}) . ":" . abs( $iv->{marquee_y} - $iv->{y}) . "]"; } elsif ( defined $current_pixel) { $str .= " $current_pixel"; } elsif ( $img and not $slideshow) { $str .= ' ' . int(100 * $zoom) . '%'; } if ( $img and not $slideshow) { my @s = map { int } ( $img-> width * $zoom, $img-> height * $zoom); if ( $s[0] != $last_size[0] or $s[1] != $last_size[1]) { @last_size = @s; $str = sprintf("[%d:%d] %s", @s, $str); } } if ( $animation) { $str .= ' ' . ($animation->{player}->current + 1) . '/' . $animation->{player}->total . ' '; } my $is_modified = $modified ? '* ' : ''; my $tag_info = (( scalar keys %tags ) ? ('[' . (scalar keys %tags) . (tag_is_set($filename) ? ':T' : '') . '] ') : '' ); my $infostr = "$tag_info$is_modified$str"; $w-> text( "FotoFix - $infostr"); $::application-> name( "FotoFix - $str"); if ( $fullscreen and $UseShapeExtension) { my $w = $iv-> FullScreenStatus; my $i = Prima::DeviceBitmap-> create( width => $w-> width, height => $w-> height, monochrome => 1, color => cl::White, backColor => cl::Black, ); $i-> clear; $i-> text_out( $infostr, 5, $i-> font-> descent); $w-> shape( $i-> image); } } sub update_menu_tags { my $x = $menu-> get_items('tagged'); if ( $x) { $menu-> remove( $_-> [0]) for @$x; } $menu-> insert( ( scalar keys %tags) ? ( [ map { my $f = $_; [ $f, sub { open_new_image($f) } ] } sort keys %tags, ] ) : ([['tagset']]), 'tagged', 0 ); $x = scalar keys %tags; $menu-> enabled( $_, $x) for qw(first_t next_t prev_t last_t); } sub update_menu_status { my $x = $image ? 1 : 0; $menu-> enabled( $_ , $x) for qw( next prev first last reopen convert copy copybits view rotate effects tag slideshow resize ); $menu-> enabled( 'palette', $image && (($image-> type & im::BPP) <= 8)); $x &&= $region; $menu-> enabled( $_, $x) for qw(crop redeyes); $x = defined $filename; $menu-> enabled( $_, $x) for qw(save delete); $menu-> enabled('save', $animation ? 0 : 1); $menu-> enabled('saveas', $animation ? 0 : $image); $menu-> enabled('animation', $animation ? 1 : 0); } sub try_max_window_size { return if 2 == grep { defined } @max_window_size; my @try_max_size = ( shift, shift ); my @adjusted_for_zoom = ( shift, shift ); my $t = $w-> bring('TryMaxWindowSizeTimer'); # exists already? timing pending? $t = $w-> insert( Timer => name => 'TryMaxWindowSizeTimer', timeout => 1, onTick => sub { my @adjusted_for_zoom = @{$_[0]-> {AdjustedForZoom}}; my @try_max_size = @{$_[0]-> {TryMaxSize}}; $_[0]-> destroy; my @actual_size = $iv-> size; for ( 0, 1) { next if defined $max_window_size[$_]; if ( $adjusted_for_zoom[$_] > $actual_size[$_]) { # window manager reduced the size $max_window_size[$_] = $actual_size[$_]; } elsif ( abs( $try_max_size[$_] - $adjusted_for_zoom[$_]) < 3) { # add lax for a couple of pixels for zoom roundoffs, # and record max size as actually reached max size, just # to stop further tries $max_window_size[$_] = $actual_size[$_]; } } } ) unless $t; $t-> {TryMaxSize} = \@try_max_size; $t-> {AdjustedForZoom} = \@adjusted_for_zoom; $t-> start; } sub update_window_size { return unless $image; if ( $ini->{WindowFit} and not $fullscreen) { my @client = get_client_size(); my $z = zoom_from_window_size( @client); my @ivsize = map { int($z * $_ + .5)} $image-> size; $w-> set( size => \@ivsize, $ini->{AutoPosition} ? ( top => $w-> top ) : () ); try_max_window_size( @client, @ivsize ); if ( $ini->{AutoPosition}) { my @fo = (0, 0); my $apph = $::application-> height; my @i = $::application-> get_indents; $apph -= $i[3]; $fo[$_] += $i[$_] for 0,1; my @fs = $w-> frameSize; $fo[1] = $apph - $fs[1]; $w-> frameOrigin( @fo); } } if ( $ini->{WindowFit} or $ini->{AutoBestFit}) { zoom_best_fit(); } elsif ( $ini->{ImageFit} and not $fullscreen) { # bring window size to the image's my @is = $image-> size; my @as = get_client_size(); zoom_set(1.0, 0); update_window_title(); $is[0] = $as[0] if $is[0] > $as[0]; $is[1] = $as[1] if $is[1] > $as[1]; for ( 0,1) { my @fo = $w-> frameOrigin; my @fs1 = $w-> frameSize; $w-> set( size => \@is, ( $ini->{AutoPosition} ? ( top => $w-> top ) : ()) ); my @fs2 = $w-> frameSize; $w-> frameOrigin( $fo[0], $fo[1] + $fs1[1] - $fs2[1]) if $ini->{AutoPosition}; # changing frame size is a tricky business, menu might wrap, # window manager might behave strangely, etc... give it # just one more try to set the minimum client size we want my @ws = $w-> size; last if $ws[0] >= $is[0] and $ws[1] >= $is[1]; $is[0] = $ws[0] if $is[0] < $ws[0]; $is[1] = $ws[1] if $is[1] < $ws[1]; } } elsif ( $ini->{ImageFit} and $fullscreen) { zoom_set 1.0; } } sub fitting_set { my ( $self, $type) = @_; $ini->{$type} = $menu-> toggle( $type); for ( qw(AutoBestFit ImageFit WindowFit)) { next if $_ eq $type; $ini->{$_} = 0; $menu-> uncheck( $_); } if ( $type ne 'ImageFit' and not $ini->{$type}) { $ini->{ImageFit} = 1; $menu-> check( 'ImageFit'); } update_window_size(); } sub scaling_set { my ( $self, $type) = @_; my $scaling = $type; $scaling =~ s/^Scaling//; $menu-> uncheck( 'Scaling' . $ini-> {Scaling}); $menu-> check( $type, $ini-> {Scaling} = $scaling); image_reset_display_buffer() if $image; } # In X11 we can only guarantee fullscreen by creating a non-WM-manageable widget. # This is portable, but we cannot bring dialogs forward, so we must deal with it # by turning the fullscreen mode off sub fullscreen_x11 { if ( shift) { $iv-> set( origin => [0,0], size => [ $::application-> size], backColor => cl::Black, owner => $::application, ); } else { $iv-> set( origin => [0,0], size => [ $w-> size], backColor => cl::Back, owner => $w, ); } } # X11 method doesn't work nice for win32, because the cursed start panel stays in front # of a non-toplevel widget, but not in front of a top-level window. Go figure. But on # a positive side, we can stop flipping back from fullscreen mode whenever we need a dialog. sub fullscreen_win32 { if ( shift) { @window_rect = $w-> rect; $w-> set( origin => [0,0], size => [ $::application-> size], backColor => cl::Black, borderIcons => 0, borderStyle => bs::None, ); $iv-> backColor( cl::Black); $menu-> selected(0); $w-> bring_to_front; } else { $w-> set( rect => \@window_rect, backColor => cl::Back, borderIcons => bi::All, borderStyle => bs::Sizeable, ); $iv-> backColor( cl::Back); $menu-> selected(1); } } sub fullscreen { my $f = $_[0] ? 1 : 0; return if $fullscreen == $f; $fullscreen = $f; magnify(0); $fullscreen_x11 ? &fullscreen_x11 : &fullscreen_win32; if ( $UseShapeExtension) { if ( $f) { $iv-> insert( Widget => name => 'FullScreenStatus', height => $iv-> font-> height, left => 0, top => $iv-> height - 5, width => $iv-> width, backColor => cl::LightGreen, visible => 0, ); update_window_title(); $iv-> FullScreenStatus-> visible(1); } else { $iv-> FullScreenStatus-> destroy; } } update_window_size(); } sub transition_block { my ( $blend1, $blend2) = @_; my $s = 8; my ( $X, $Y) = ( int( $blend1-> width / $s), int( $blend2-> height / $s)); my $n = $X * $Y; my @p = ((1) x $n); my $left = $n; while ( $left > 0) { my $i = int rand($n); next unless $p[$i]; $p[$i] = undef; $left--; my ( $x, $y) = map { int($_) * $s} ( $i / $Y, $i % $Y); $iv-> put_image_indirect( $blend2, $x, $y, $x, $y, $s, $s, $s, $s, rop::CopyPut ); } } sub transition_blend { my ( $blend1, $blend2) = @_; return if $blend1-> type != im::RGB or $blend2-> type != im::RGB; $iv-> put_image( 0, 0, combine_channels( [$blend2,$blend1], 'alpha' . ( $_ * 16 - 1) )) for 1..15; } sub transition_images { return transition_block( @_) if $ini-> {Transition} eq 'block'; return transition_blend( @_) if $ini-> {Transition} eq 'blend'; return if $ini-> {Transition} eq 'none'; die "unknown transition: $ini->{Transition}\n"; } sub close_image { undef $region; undef $image; undef $filename; undef $filecodec; animation_close(); $modified = 0; %neighbour_files_cache = (); } sub open_image { return if $modified and not can_close_image(); my $save = $image; $open_dialog = Prima::ImageOpenDialog-> create( onHeaderReady => sub { $image = $_[1]; update_window_size(); }, ) unless $open_dialog; $open_dialog-> directory($ini-> {Path}); $image_is_loading++; my $i = $open_dialog-> load( progressViewer => $iv, wantFrames => 1, loadExtras => 1, ); $image_is_loading--; unless ($i) { if (( $image || 0) != ( $save || 0)) { $image = $save; update_window_size(); } return; } undef $save; close_image(); $filename = $open_dialog-> fileName; $filecodec = $i-> {extras}->{codecID}; %tags = () if ( $ini-> {Path} || '') ne $open_dialog-> directory; $ini-> {Path} = $open_dialog-> directory; $zoom = 1.0; return 1 if $open_dialog-> {frameIndex} == 0 and image_is_animation($i) and animation_load($filename); $image = $i; image_reset_display_buffer(); update_window_size(); update_window_title(); update_menu_status(); last_file_add( $filename); $iv-> update_view; } sub open_new_image { my ( $fn, %opt) = @_; my $save = $image; my $i = Prima::Image-> new; my $ok; if ( not($opt{slideshow}) and $ini-> {ShowPartial}) { $image_is_loading++; $iv-> watch_load_progress( $i); my $id = $i-> add_notification( 'HeaderReady', sub { $image = $i; image_reset_display_buffer(); update_window_size; }); $ok = $i-> load($fn, loadExtras => 1, wantFrames => 1); $i-> remove_notification( $id); $iv-> unwatch_load_progress; $image_is_loading--; } else { $ok = $i-> load($fn, loadExtras => 1, wantFrames => 1); } # failed unless ($ok) { if (( $image || 0) != ( $save || 0)) { $image = $save; update_window_size(); } message( "Cannot load image $fn:$@"); return 0; } # succeeded my $there_was_image = $save ? 1 : 0; undef $save; close_image(); $filename = $fn; $filecodec = $i-> {extras}->{codecID}; $zoom = 1.0; if ( $opt{slideshow} and ($ini-> {Transition} ne 'none') and ($fullscreen || $ini-> {WindowFit} || $ini-> {AutoBestFit}) and $there_was_image ) { $iv-> begin_paint; # and onPaint won't be called by the system # create blend images my ( $blend1, $blend2); my @r = $iv-> client_to_screen( 0, 0, $iv-> size); $blend1 = $::application-> get_image( @r); $image = $i; image_reset_display_buffer(); update_window_size(); $blend2 = $blend1-> dup; $blend2-> begin_paint; $blend2-> $_( $iv-> $_) for qw(color backColor); $iv-> on_paint( $blend2); $blend2-> end_paint; # do the transition $w-> SlideshowTimer-> stop if $w-> SlideshowTimer; transition_images( $blend1, $blend2); $w-> SlideshowTimer-> start if $w-> SlideshowTimer; $iv-> end_paint; } return 1 if image_is_animation($i) and animation_load($filename); $image = $i; image_reset_display_buffer(); update_window_size(); update_window_title(); update_menu_status(); $iv-> update_view; return 1; } sub reopen_image { open_new_image($filename) if $filename } sub get_dir_list { my ( $basedir, $file) = ( $filename =~ /^(.*)[\\\/]([^\\\/]+)$/ ) ? ($1,$2) : ('.',$filename); my $exts = join('|', map { @{$_->{fileExtensions}} } @{Prima::Image-> codecs}); my $rx = qr/\.($exts)$/i; return unless opendir D, $basedir; my @files = grep { /$rx/ } sort readdir D; closedir D; my $found; for ( my $i = 0; $i < @files; $i++) { next unless $files[$i] eq $file; $found = $i; last; } return $basedir, $found, @files; } sub get_next_image_index { my ( $next, $current_files) = @_; my $sign = $next ? 1 : -1; my ( $min_dist, $found_index, $i); $i = -1; for my $file ( @$current_files) { $i++; next unless $neighbour_files_cache{ $file }; my $distance = $sign * $neighbour_files_cache{ $file }; next if $distance < 0; ($min_dist,$found_index) = ( $distance,$i) if not defined($min_dist) or $min_dist > $distance; } if ( defined $found_index) { return $found_index - $sign; } else { return $next ? $#$current_files : 0; } } sub populate_next_image_cache { my ( $current_index, $current_files) = @_; my $i; %neighbour_files_cache = (); return unless defined $current_index; for ( $i = 0; $i < @$current_files; $i++) { $neighbour_files_cache{ $current_files->[$i] } = $i - $current_index; } } sub open_next_image { my ( $self, $menu) = @_; return if $modified and not can_close_image(); return unless defined $filename; my ( $basedir, $index, @files) = get_dir_list(); return message("No files found") unless @files; if ( $menu eq 'prev') { $index = get_next_image_index( 0, \@files) unless defined $index; if ( $index == 0) { return if message("First image in the directory, go to the last?", mb::YesNo) != mb::Yes; $index = $#files; } else { $index--; } } elsif ( $menu eq 'next') { $index = get_next_image_index( 1, \@files) unless defined $index; if ( $index == $#files) { return if message("Last image in the directory, go to the first?", mb::YesNo) != mb::Yes; $index = 0; } else { $index++; } } elsif ( $menu eq 'first') { $index = 0; } elsif ( $menu eq 'last') { $index = $#files; } open_new_image( "$basedir/$files[$index]"); if ( $slideshow) { $w-> SlideshowTimer-> stop; $w-> SlideshowTimer-> start; } } sub save_image { unless ( $image-> save( $filename)) { message('Cannot save '.$filename . ":$@"); return 0; } $modified = 0; update_window_title(); 1; } sub save_image_as { my $ok; $image-> {extras}->{codecID} = $filecodec; $save_dialog = Prima::ImageSaveDialog-> create() unless $save_dialog; $save_dialog-> set( directory => $ini-> {Path}, image => $image, ); if ( $save_dialog-> save( $image)) { $filename = $save_dialog-> fileName; $modified = 0; $ok = 1; $ini-> {Path} = $save_dialog-> directory; update_window_title(); } $ok; } sub filename2tag { my $fn = shift; return undef unless defined $fn; eval { $fn = abs_path( $fn); }; # it may croak if file's not found! what a fuckup if ( $^O =~ /win32/i) { $fn = lc $fn ; # oh yeah $fn =~ s/\\/\//g; } return $fn; } sub tag_is_set { return defined($_[0]) ? exists $tags{ filename2tag( $_[0] ) } : undef } sub tags_toggle_image { return unless defined $filename; my $f = filename2tag( $filename); if ( exists $tags{ $f }) { delete $tags{ $f }; } else { $tags{ $f } = 1; }; update_window_title(); update_menu_tags(); } sub tags_clear { %tags = (); update_window_title(); update_menu_tags(); } sub tags_invert { my ( $basedir, undef, @files) = get_dir_list(); my %t; # in case some leftovers are there for ( @files) { my $fn = filename2tag("$basedir/$_"); $t{ $fn } = 1 unless exists $tags{ $fn }; } %tags = %t; update_window_title(); update_menu_tags(); } sub open_next_tagged_image { my ( $self, $menu) = @_; return if $modified and not can_close_image(); return unless defined $filename; my ( $basedir, $index, @files) = get_dir_list(); return message("No files found") unless @files; return message("No tagged files") unless scalar keys %tags; if ( $menu eq 'prev_t' or $menu eq 'last_t') { if ( $menu eq 'prev_t') { $index = get_next_image_index( 0, \@files) unless defined $index; } else { $index = $#files; } my $i = $index; while ( 1) { if ( $i == 0) { return if message( "First tagged image in the directory, go to the last?", mb::YesNo ) != mb::Yes; $i = $#files; } else { $i--; } return message("Cannot find any tagged file") if $index == $i; my $f = filename2tag("$basedir/$files[$i]"); if ( $tags { $f }) { $index = $i; last; } } } elsif ( $menu eq 'next_t' or $menu eq 'first_t') { if ( $menu eq 'next_t') { $index = get_next_image_index( 1, \@files) unless defined $index; } else { $index = 0; } my $i = $index; while ( 1) { if ( $i == $#files) { return if message( "Last tagged image in the directory, go to the first?", mb::YesNo ) != mb::Yes; $i = 0; } else { $i++; } return message("Cannot find any tagged file") if $index == $i; my $f = filename2tag("$basedir/$files[$i]"); if ( $tags { $f }) { $index = $i; last; } } } open_new_image( "$basedir/$files[$index]"); } sub files_get_selection { if ( keys %tags) { return sort keys %tags; } elsif ( defined $filename) { return ( $filename); } else { message("No tagged files, no open files, nothing to do"); return (); } } sub files_multirun { my ( $title, $sub, @files) = @_; my $i = 1; my $n = @files; for my $f ( @files) { my $t = "$title $i of $n: $f"; $w-> text( $f ); $::application-> name( $f ); $i++; my $res = $sub-> ( $f ); last unless $res; } update_window_title(); } sub files_copy_move { my $op = shift; my $name = ucfirst $op; require Prima::FileDialog; eval { require File::Copy; }; return message( $@) if $@; my @f = files_get_selection(); return unless @f; $chdir_dialog = Prima::ChDirDialog-> new() unless $chdir_dialog; $chdir_dialog-> set( text => "$name " . (( 1 == @f) ? $f[0] : scalar(@f) . ' files') . ' to...', directory => $ini-> {ChdirPath}, ); return unless $chdir_dialog-> execute == mb::Ok; my $dir = $ini-> {ChdirPath} = $chdir_dialog-> directory; return message("No such directory '$dir'") unless -d $dir; if ( $op eq 'move') { my ( $basedir, $index, @files) = get_dir_list(); populate_next_image_cache( $index, \@files); } my $YesToAll = 0; my $what = (( $op eq 'copy') ? 'Copying' : 'Moving'); files_multirun( $what, sub { my $src = $_[0]; $src =~ /([^\\\/]*)$/; my $dst = "$dir/$1"; if ( -f $dst and not $YesToAll) { my $r = message_box( $what, "$dst already exists. Overwrite?", mb::YesNo|mb::Abort|mb::Ignore|mb::Warning, { buttons => { mb::Ignore => { text => 'Yes to all', } }, } ); return 0 if $r == mb::Abort; return 1 if $r == mb::No; $YesToAll++ if $r == mb::Ignore; } my $ok; RETRY: $ok = File::Copy-> can($op)-> ( $src, $dst); unless ( $ok) { my $r = message_box( $what, "Error " . lcfirst($what) . " $src to $dir: $^E", mb::Abort|mb::Retry|mb::Cancel ); return 0 if $r == mb::Abort; return 1 if $r == mb::Cancel; goto RETRY; } else { delete $tags{ filename2tag( $src ) }; } return $ok; }, @f); update_menu_tags(); update_window_title(); } sub files_copy { files_copy_move( 'copy' ) } sub files_move { files_copy_move( 'move' ) } sub files_rename_exec { my ( $what, $cmd, @files) = @_; my $sub = eval { eval "sub { $cmd; }" }; return message( $@) if $@; my ( $basedir, $index, @f) = get_dir_list(); populate_next_image_cache( $index, \@f); local $_; local $. = -1; for my $f ( @files) { my ( $path, $basename) = $f =~ m/^(.*)[\\\/]([^\\\/]*)$/; $.++; $_ = $basename; $sub->(); next if $f eq $_; my $n = "$path/$_"; my $ok; RETRY: $ok = rename( $f, $n); unless ( $ok) { my $r = message_box( $what, "Error renaming $f to $n:$!", mb::Abort|mb::Retry|mb::Cancel ); return 0 if $r == mb::Abort; return 1 if $r == mb::Cancel; goto RETRY; } else { delete $tags{ filename2tag( $f ) }; } $w-> text( "$what $f to $n..." ); $::application-> name( "$what $f to $n..." ); update_window_title(); } } sub files_rename { my @f = files_get_selection(); return unless @f; my $cmd = input_box( 'Rename '. (( 1 == @f) ? $f[0] : scalar(@f) . ' files to'), 'Perl regular expression:', '', mb::OkCancel|mb::Help, { helpTopic => "$0/Rename", }); return unless defined $cmd and length $cmd; files_rename_exec('Rename', $cmd, @f); } sub files_prefix { my @f = files_get_selection(); return unless @f; my $cmd = input_box( 'Prefix '. (( 1 == @f) ? $f[0] : scalar(@f) . ' files with'), 'prefix:', '', mb::OkCancel ); return unless defined $cmd and length $cmd; files_rename_exec('Prefix', "s/^/$cmd/", @f ); } sub files_delete { my @f = files_get_selection(); return unless @f; return unless mb::Ok == message_box( 'Deleting', "Really delete " . (( 1 == @f) ? $f[0] : scalar(@f) . ' files') . ' ?', mb::OkCancel|mb::Warning ); my ( $basedir, $index, @files) = get_dir_list(); populate_next_image_cache( $index, \@files); files_multirun( 'Deleting', sub { my $ok; RETRY: $ok = unlink $_[0]; unless ( $ok) { my $r = message_box( 'Deleting', "Error deleting $_[0]:$!", mb::Abort|mb::Retry|mb::Cancel ); return 0 if $r == mb::Abort; return 1 if $r == mb::Cancel; goto RETRY; } else { delete $tags{ filename2tag( $_[0] ) }; } }, @f); update_menu_tags(); update_window_title(); } sub files_execute { my @f = files_get_selection(); return unless @f; my $cmd = input_box( 'Execute command on '. (( 1 == @f) ? $f[0] : scalar(@f) . ' files'), 'Command:', '', mb::OkCancel|mb::Help, { helpTopic => "$0/Execute", }); return unless defined $cmd and length $cmd; # we don't know if the command will be destructive or not, so we'll cache just in case my ( $basedir, $index, @files) = get_dir_list(); populate_next_image_cache( $index, \@files); if ( $cmd =~ /\$_/) { for my $f ( @f) { my $c = $cmd; $c =~ s/\$_/$f/g; $w-> text( $c ); $::application-> name( $c ); update_window_title(); next if 0 == system $c; message_box( 'Execute', "'$c' failed: error code $?"); last; } } else { $cmd .= ' $*' unless $cmd =~ /\$\*/; my $list = join(' ', @f); $cmd =~ s/\$\*/$list/g; $w-> text( $cmd ); $::application-> name( $cmd ); (0 == system($cmd)) or message_box('Execute', "'$cmd' failed: error code $?"); } update_window_title(); } sub can_close_image { return 1 unless $modified; my $ret; if ( $filename) { $ret = message( "Image $filename wasn't saved. Save?", mb::YesNoCancel ); return 1 if $ret == mb::Yes and save_image(); } else { $ret = message( "Untitled image wasn't saved. Save?", mb::YesNoCancel ); return 1 if $ret == mb::Yes and save_image_as(); } return 1 if $ret == mb::No; 0; } sub on_close { shift-> clear_event unless can_close_image() } sub iv_mousedown { my ( $self, $btn, $mod, $x, $y) = @_; return if $self-> {drag} or not $image; if ( $btn == mb::Right) { $self-> {x} = $x; $self-> {y} = $y; $self-> {wasdx} = $self-> deltaX; $self-> {wasdy} = $self-> deltaY; $self-> pointer( $icons{hand}); $self-> capture(1); } elsif ( $btn == mb::Left) { @{$self}{qw(x y marquee_x marquee_y)} = map { int } convert_screen_to_point( $x, $y, $x, $y); $self-> capture(1, $self); } elsif ( $btn == mb::Middle) { return magnify( $magnify ? 0 : 1); } else { return; } magnify(0); $self-> {drag} = $btn; } sub iv_mouseup { my ( $self, $btn, $mod, $x, $y) = @_; return unless $self-> {drag} && $btn == $self->{drag}; return if $btn == mb::Middle; $self-> {drag} = 0; $self-> capture(0); update_window_title(); if ( $btn == mb::Right) { $self-> pointer( cr::Default); } elsif ( $btn == mb::Left) { draw_marquee(); delete @{$self}{qw(marquee_x marquee_y)}; region_set( @{$self}{qw(x y)}, convert_screen_to_point( $x, $y)); update_menu_status(); } } sub iv_mousemove { my ( $self, $mod, $x, $y) = @_; if ( not $self-> {drag}) { if ( $mod & km::Shift) { my @p = convert_screen_to_point($x, $y); my $p = (grep { $_ < 0 } @p) ? cl::Invalid : $image-> pixel( @p); @p = map { int } @p; $current_pixel = ( $p == cl::Invalid) ? undef : sprintf( "$p[0]:" . ($image-> height - $p[1] - 1) . ' ' . $image_format_category{$image->type & im::Category}, $image-> pixel( convert_screen_to_point($x, $y)) ); $::application-> pointerVisible(1) if $magnify and not $::application-> pointerVisible; update_window_title(); } elsif ( defined $current_pixel) { undef $current_pixel; update_window_title(); } if ( $magnify) { $magnify-> origin( $x - $MagnifyingGlassSize[0]/2, $y - $MagnifyingGlassSize[1]/2, ); $magnify-> repaint; $self-> update_view; } } elsif ( $self-> {drag} == mb::Right) { my ($dx,$dy) = ($x - $self-> {x}, $y - $self-> {y}); $self-> deltas( $self-> {wasdx} - $dx, $self-> {wasdy} + $dy); } elsif ( $self-> {drag} == mb::Left) { draw_marquee(); @{$self}{qw(marquee_x marquee_y)} = map { int } convert_screen_to_point($x, $y); draw_marquee(); update_window_title(); } } sub iv_mousewheel { my ( $self, $mod, $x, $y, $z) = @_; if ( $magnify) { $z = int( $z / 120); if ( $z > 0 ) { $magnify_zoom *= 0.9; $magnify_zoom = 1 if $magnify_zoom < 1; } else { $magnify_zoom *= 1.1; $magnify_zoom = 100 if $magnify_zoom > 100; } $magnify-> repaint; } else { $z = 5 * int( $z / 120); my $xv = ($mod & km::Shift) ? 'vScroll' : 'hScroll'; return unless $self-> $xv(); $xv = $self-> bring( ucfirst $xv); $z *= ($mod & km::Ctrl) ? $xv-> step : $xv-> pageStep; my $meth = ( $mod & km::Shift) ? 'deltaX' : 'deltaY'; $self-> $meth( $self-> $meth - $z); } } sub iv_keydown { my $self = shift; if ( $fullscreen) { my ( $code, $key, $mod, $rep) = @_; if ( $key == kb::Enter) { fullscreen(0); $self-> clear_event; } else { $w-> key_down(@_); } } } sub iv_paint { my ( $self, $canvas) = @_; $self-> on_paint( $canvas); $canvas-> translate(0,0); if ( $fullscreen and not($UseShapeExtension) and $filename) { $canvas-> color( cl::LightGreen); $canvas-> text_out( $filename, 10, $canvas-> height - $canvas-> font-> width - 10); } if ( $region) { $canvas-> color( cl::Set); $canvas-> rop( rop::XorPut); $canvas-> rectangle( convert_point_to_screen( @$region)); } } sub iv_size { my ( $self, $ox, $oy, $x, $y) = @_; return unless $iv; return unless $ini->{WindowFit} or $ini->{AutoBestFit}; # compress resize events return if $self-> bring('ResizeTimer'); $self-> insert( Timer => name => 'ResizeTimer', timeout => 1, onTick => sub { shift-> destroy; zoom_best_fit; } )-> start; } sub conversion_set { my ( $self, $menuID) = @_; return if $conversion_menuid eq $menuID; $self-> menu-> uncheck( $conversion_menuid); $self-> menu-> check( $menuID); $conversion_menuid = $menuID; $conversion = ( ( $menuID eq 'N') ? ict::None : ( ( $menuID eq 'O') ? ict::Ordered : ( ( $menuID eq 'E') ? ict::ErrorDiffusion : ict::Optimized ))); } sub image_convert { my $type = shift; my %set; unless ( $type) { my $now_colors = ( $image-> get_bpp > 8) ? 256 : scalar $image-> colormap; my $colors = input_box( 'Convert image', 'Reduce colors to (2-256):', $now_colors, mb::OkCancel ); return unless defined $colors and length $colors; message("Number required"), redo unless $colors =~ /^\d+(\.\d+)?$/; message("Value between 2 and 256"), redo unless $colors >= 2 and $colors <= 256; return if $colors == $now_colors; $set{palette} = $colors; if ( $colors > 16) { $type = 8; } elsif ( $colors > 2) { $type = 4; } else { $type = 1; } } $image-> set( conversion => $conversion, type => $type, %set, ); image_replace( $image); } sub image_rotate { return unless loadIPA; my $d = shift; my $i; if ( $d == 90) { $i = rotate90( $image, 1); } elsif ( $d == 180) { $i = rotate180( $image); } elsif ( $d == 270) { $i = rotate90( $image, 0); } else { die "invalid call to image_rotate:$d\n"; } image_replace($i); } sub image_is_animation { my $i = shift; return 0 unless # more than 1 frame? $i-> {extras} && defined($i-> {extras}->{codecID}) && $i-> {extras}->{frames} && $i-> {extras}->{frames} > 1; $codecs ||= Prima::Image-> codecs; my $c = $codecs-> [ $i-> {extras}-> {codecID} ]; return 0 unless # is it really libungif codec? $c && $c-> {name} eq 'GIFLIB'; eval { require Prima::Image::AnimateGIF; }; message($@), return 0 if $@; return 1; } sub animation_load { my $filename = shift; my $player = Prima::Image::AnimateGIF-> load($filename); message("Cannot load $filename as animation:$@"), return unless $player; my $info = $player-> next; return unless $info; my $i = $player-> image; return unless $i; # commit $animation-> {timer} = Prima::Timer-> new( timeout => $info-> {delay} * 1000, onTick => sub { if ( $animation && $animation-> {player}) { $info = $animation-> {player}-> next; if ( $info) { $_[0]-> timeout( $info-> {delay} * 1000); $image = $player-> image; image_reset_display_buffer(); $magnify-> repaint if $magnify; update_window_title(); } else { $_[0]-> stop; } } }, ); $image = $i; image_reset_display_buffer(); $animation-> {player} = $player; $animation-> {timer}-> start; update_window_size(); update_window_title(); update_menu_status(); $iv-> update_view; return 1; } sub animation_close { return unless $animation; ## uncomment if you happen to erase your animation gifs too often # $filename = sprintf "frame%d-%s", # $animation-> {player}-> current, # $filename # if defined $filename; $animation-> {timer}-> destroy if $animation-> {timer}; undef $animation; update_menu_status(); return 1; } sub animation_toggle { return unless $animation; my $t = $animation-> {timer}; $t-> get_active ? $t-> stop : $t-> start; } sub animation_next { return unless $animation; $animation-> {timer}-> notify('Tick'); } sub animation_rewind { return unless $animation; $animation-> {player}-> reset; animation_next; } my $ResizeFM = # This is resize.fm included as is in its entirety. If you want to edit it # with VB, just copy it to a file. # <--- cut from here ---> # VBForm version file=1.2 builder=0.2 # [preload] Prima::ComboBox sub { return ( 'Label1' => { class => 'Prima::Label', module => 'Prima::Label', siblings => [qw(focusLink)], profile => { owner => 'Form1', text => '~Width', focusLink => 'Width', origin => [ 8, 180], name => 'Label1', size => [ 143, 23], }}, 'Form1' => { class => 'Prima::Window', module => 'Prima::Classes', parent => 1, profile => { sizeDontCare => 0, text => 'Resize image', name => 'Form1', origin => [ 817, 580], originDontCare => 0, size => [ 467, 214], }}, 'Label2' => { class => 'Prima::Label', module => 'Prima::Label', siblings => [qw(focusLink)], profile => { owner => 'Form1', text => '~Height', focusLink => 'Height', name => 'Label2', origin => [ 8, 132], size => [ 143, 23], }}, 'Scaling' => { class => 'Prima::ComboBox', module => 'Prima::ComboBox', profile => { owner => 'Form1', tabOrder => 4, text => '', name => 'Scaling', origin => [ 160, 156], style => cs::DropDownList, size => [ 143, 23], }}, 'Label3' => { class => 'Prima::Label', module => 'Prima::Label', siblings => [qw(focusLink)], profile => { owner => 'Form1', text => '~Scaling', focusLink => 'Scaling', name => 'Label3', origin => [ 160, 180], size => [ 143, 23], }}, 'Color' => { class => 'Prima::ColorComboBox', module => 'Prima::ColorDialog', profile => { owner => 'Form1', tabOrder => 5, name => 'Color', origin => [ 160, 108], size => [ 143, 23], }}, 'Label4' => { class => 'Prima::Label', module => 'Prima::Label', siblings => [qw(focusLink)], profile => { owner => 'Form1', focusLink => 'Color', text => '~Color fill', name => 'Label4', origin => [ 160, 132], size => [ 143, 23], }}, 'Proportional' => { class => 'Prima::CheckBox', module => 'Prima::Buttons', profile => { owner => 'Form1', tabOrder => 2, text => '~Proportional', name => 'Proportional', origin => [ 8, 84], size => [ 143, 23], }}, 'Stretch' => { class => 'Prima::CheckBox', module => 'Prima::Buttons', profile => { owner => 'Form1', tabOrder => 3, text => 'S~tretch', origin => [ 8, 60], name => 'Stretch', size => [ 143, 23], }}, 'Placement' => { class => 'Prima::ComboBox', module => 'Prima::ComboBox', profile => { owner => 'Form1', style => cs::DropDownList, origin => [ 160, 60], name => 'Placement', size => [ 143, 23], tabOrder => 6, text => 'Center Center', items => ['Top Left', 'Top Center', 'Top Right', 'Center Left', 'Center Center', 'Center Right', 'Bottom Left', 'Bottom Center', 'Bottom Right', ], }}, 'Label5' => { class => 'Prima::Label', module => 'Prima::Label', siblings => [qw(focusLink)], profile => { owner => 'Form1', text => 'P~lacement', focusLink => 'Scaling', name => 'Label5', origin => [ 160, 84], size => [ 143, 23], }}, 'Width' => { class => 'Prima::SpinEdit', module => 'Prima::Sliders', profile => { owner => 'Form1', tabOrder => 0, min => 1, max => 16384, origin => [ 8, 156], name => 'Width', size => [ 143, 23], }}, 'Height' => { class => 'Prima::SpinEdit', module => 'Prima::Sliders', profile => { owner => 'Form1', tabOrder => 1, min => 1, max => 16384, name => 'Height', origin => [ 8, 108], size => [ 143, 23], }}, 'Display' => { class => 'Prima::Widget', module => 'Prima::Classes', profile => { owner => 'Form1', tabOrder => 7, name => 'Display', origin => [ 312, 60], size => [ 144, 144], }}, 'Button1' => { class => 'Prima::Button', module => 'Prima::Buttons', profile => { owner => 'Form1', tabOrder => 9, modalResult => '4', text => 'Cancel', origin => [ 356, 10], name => 'Button1', size => [ 96, 36], }}, 'Button2' => { class => 'Prima::Button', module => 'Prima::Buttons', profile => { owner => 'Form1', tabOrder => 8, modalResult => '1', text => '~OK', origin => [ 256, 10], name => 'Button2', default => 1, size => [ 96, 36], }}, ); } # <--- stop cutting ---> ; # given placement string, image size, and fitting size, # returns the suggested placement coordinates sub image_resize_calculate_placement { my ( $string, $x, $y, $w, $h) = @_; my ( $V, $H) = $string =~ /^(\w+)\s+(\w+)$/; my ( $X, $Y); if ( $H eq 'Right') { $X = $w - $x; } elsif ( $H eq 'Center') { $X = int(( $w - $x) / 2); } else { $X = 0; } if ( $V eq 'Top') { $Y = $h - $y; } elsif ( $V eq 'Center') { $Y = int(( $h - $y) / 2); } else { $Y = 0; } return ( $X, $Y); } sub image_resize { require Prima::VB::VBLoader; my %form; my @is = ( $image-> width, $image-> height); my @cm = $image-> colormap; %form = Prima::VB::VBLoader::AUTOFORM_REALIZE( [ $ResizeFM-> () ], { Form1 => { visible => 0, designScale => [ 9, 19 ] }, Width => { value => $is[0], onChange => sub { $form{Height}-> value( int( shift-> value * $is[1] / $is[0] + .5 )) if $form{Proportional}-> checked; $form{Display}-> repaint; }, }, Height => { value => $image-> height, onChange => sub { $form{Width}-> value( int( shift-> value * $is[0] / $is[1] + .5 )) if $form{Proportional}-> checked; $form{Display}-> repaint; }, }, Stretch => { onCheck => sub { my $enable = ! shift-> checked; $form{$_}-> enabled( $enable) for qw(Color Placement); $form{Display}-> repaint; }}, Color => { onChange => sub { $form{Display}-> repaint }, scalar(@cm) ? ( colors => scalar(@cm), onColorify => sub { my ( $self, $index, $ptr) = @_; $$ptr = $cm[$index]; }) : (), }, Placement => { onChange => sub { $form{Display}-> repaint }}, Proportional => { onCheck => sub { $form{Width}-> notify(qw(Change)) if shift-> checked }}, Scaling => { items => [ 'Nearest neighborhood', $UseImageMagick ? ( qw( Quadratic Cubic Triangle Hermite Hanning Hamming Blackman Gaussian Catrom Mitchell Lanczos Bessel Sinc)) : (), ], text => $ini-> {Scaling}, }, Display => { onPaint => sub { my ( $self, $canvas) = @_; my @new = ( $form{Width}-> value, $form{Height}-> value); my @sz = $self-> size; my @ix = map { $sz[$_] / $new[$_] } (0,1); # actual scale to display the image my @rect = ( 0, 0, @sz); my $scale = ( $ix[0] < $ix[1]) ? $ix[0] : $ix[1]; my @panel = map { $scale * $new[$_] } (0,1); # new image scaled down @ix = map { $scale * $is[$_] } (0,1); # old image scaled down for ( 0, 1) { $rect[$_] = ( $sz[$_] - $panel[$_] ) / 2; $rect[$_ + 2] = $rect[$_] + $panel[$_]; } $canvas-> clear; for ( @panel, @ix) { $_ = int( $_ + .5); $_++ unless $_; } if ( $form{Stretch}-> checked) { $canvas-> stretch_image( @rect[0,1], @panel, $image); } else { $canvas-> backColor( $form{Color}-> value); $canvas-> clear( @rect); my @place = image_resize_calculate_placement( $form{Placement}-> text, @ix, @panel ); $canvas-> clipRect( @rect); $canvas-> stretch_image( $place[0] + $rect[0], $place[1] + $rect[1], @ix, $image ); } }}, }); return message($@) unless $form{Form1}; goto DONE if $form{Form1}-> execute != mb::OK; # resizing now! my @new = ( $form{Width}-> value, $form{Height}-> value); goto DONE if $new[0] == $is[0] && $new[1] == $is[1]; # do nothing # stretch if ( $form{Stretch}-> checked) { if ( $form{Scaling}-> text !~ /^Nearest/) { my $g = Prima::Image::Magick::prima_to_magick( $image); $g-> Resize( width => $new[0], height => $new[1], filter => $form{Scaling}-> text, ); $image = $g-> Prima; } else { $image-> size( @new); } image_replace( $image); goto DONE; } # no stretch my @place = image_resize_calculate_placement( $form{Placement}-> text, @is, @new ); # simple extraction of a smaller part if ( $place[0] <= 0 && $place[1] <= 0) { image_replace( $image-> extract( -$place[0], -$place[1], @new)); goto DONE; } # extract part of image and superimpose on larger one my $i = Prima::Image-> create( width => $new[0], height => $new[1], palette => $image-> palette, type => $image-> type, color => $form{Color}-> value, ); # we're guaranteed that a new image is black; change all black pixels to given color $i-> map(0); $i-> put_image( @place, $image); image_replace( $i); DONE: $form{Form1}-> destroy; } sub image_mirror { return unless loadIPA; image_replace( mirror( $image, type => 1 + shift)); } sub image_invert { # could just as well invert the palette if possible, but # probably there are chances that it is data to be inverted... # doesn't work on floats return message('Unimplemented') if im::RealNumber <= ($image-> type & im::Category); if ( $region) { my $i = region_image(); $i-> data( ~$i-> data); $image-> put_image( @$region[0,1], $i); } else { $image-> data( ~$image-> data); } image_replace($image); } sub image_remove_red_eyes { return unless loadIPA; return message('Can only work on color images') if im::Color != ($image-> type & im::Category); my $i = region_image(); $i-> type( im::RGB); # split_channels accepts RGB only my ( $r, $g, $b) = @{split_channels( $i)}; my ( $G, $B); if ( $RedEyesHueDiff < 0.9999 or $RedEyesHueDiff > 1.001) { $G = $g-> dup; $g-> type(im::Short); $g = ab( $g, $RedEyesHueDiff, 0); $g = threshold( $g, false => 255, maxvalue => 255, preserve => 1); $g-> type(im::Byte); $B = $b-> dup; $b-> type(im::Short); $b = ab( $b, $RedEyesHueDiff, 0); $b = threshold( $b, false => 255, maxvalue => 255, preserve => 1); $b-> type(im::Byte); } else { ( $G, $B) = ( $g, $b); } # keep strong red features by subtracting everything green AND blue my $x = subtract( $r, $g, conversionType => IPA::conversionTrunc()); $x = subtract( $x, $b, conversionType => IPA::conversionTrunc()); # have a binary mask $x = threshold( $x, minvalue => 1); # cut a hole with the mask in the original red channel $r-> rop( rop::NotSrcAnd); $r-> put_image( 0, 0, $x); # create an averaged green/blue patch my $gb = average([ $b, $g ]); $gb-> rop( rop::AndPut); $gb-> put_image( 0, 0, $x); # plaster this patch over a hole in the red channel $r-> rop( rop::OrPut); $r-> put_image( 0, 0, $gb); # combine back $i = combine_channels([$r,$G,$B], 'rgb'); # put the area back to the big image $i-> type( $image-> type); $image-> put_image( @$region[0,1], $i); undef $region; image_replace($image); } sub magnify { my $show = $_[0]; if ( $show) { return if $magnify; my $x = $w-> insert( Widget => size => \@MagnifyingGlassSize, syncPaint => 1, buffered => 1, clipOwner => not($fullscreen), onMouseWheel => \&iv_mousewheel, onMouseMove => sub { magnify(0) }, # if capture was superseded by WM onPaint => sub { my ( $self, $canvas) = @_; $self-> clear; if ( $image) { my @m = map { $_ - 2 } @MagnifyingGlassSize; my $z = $zoom * $magnify_zoom; my @c = map { $_ / $z } @m; my @i = convert_screen_to_point( $iv-> pointerPos); $i[$_] -= $c[$_] / 2 for 0,1; my @d = (1,1); if ( $zoom > 1) { for ( 0,1) { $d[$_] -= ($i[$_] - int($i[$_])) * $z; $c[$_]++; $m[$_] += $z; } } $self-> put_image_indirect( $image, @d, @i, @m, @c, rop::CopyPut ); } $self-> rectangle( 0, 0, map { $_ - 1 } @MagnifyingGlassSize); }, ); $magnify_zoom = 2; $x-> focus; $iv-> capture(1); $::application-> pointerVisible(0); $magnify = $x; } else { return unless $magnify; $iv-> capture(0); $::application-> pointerVisible(1) unless $::application-> pointerVisible; $magnify-> destroy; $iv-> select; undef $magnify; } } sub grab_screen { return if $modified and not can_close_image; my $delay = 2; message_box( 'Grab screen', join(' ', (split "\n", < visible(0); for ( 1..10) { sleep($delay); $::application-> yield; last unless $::application-> get_shift_state & km::Ctrl; } my $x = $::application-> get_image( 0, 0, $::application-> size); $w-> visible(1); unless ( $x) { message("Cannot grab image"); return; } $filename = 'screenshot' unless defined $filename; image_replace( $x); } sub edit_palette { return message("Cannot edit palette on this image") unless $image and (($image-> type & im::BPP) <= 8); my $was_grayscale; # grayscale palette is locked if ( $image-> type & im::GrayScale) { $image-> type( $image-> type & ~im::GrayScale); $was_grayscale = 1; } require Prima::Grids; require Prima::ColorDialog; my $fh = $w-> font-> height; my @ext = ( 16, 16); my ( $cd, $curr_index, @colormap, $old_image, $touch ); my @current = @colormap = $image-> colormap; while ( $ext[0] * $ext[1] > @colormap) { $ext[1]--; if ( $ext[0] * $ext[1] < @colormap) { $ext[1]++; last; } } my $d = Prima::Dialog-> new( text => 'Edit palette', size => [25 * $ext[0] + 4, 25 * $ext[1] + $fh * 4], ); my $grid = $d-> insert( GridViewer => origin => [0,0], size => [$d-> size], constantCellWidth => 24, constantCellHeight => 24, multiSelect => 0, cells => [([(undef) x $ext[0]]) x $ext[1]], drawHGrid => 0, drawVGrid => 0, onDrawCell => sub { my ( $self, $canvas, $col, $row, $indent, $sx1, $sy1, $sx2, $sy2, $cx1, $cy1, $cx2, $cy2, $selected, $focused ) = @_; my $index = $row * $ext[0] + $col; $canvas-> backColor( ($index > $#colormap) ? cl::Back : $current[$index] ); $canvas-> clear($sx1, $sy1, $sx2, $sy2); return if $index >= @colormap; $canvas-> rectangle( $cx1-1, $cy1-1, $cx2, $cy2); $canvas-> rect_focus( $sx1, $sy1, $sx2-1, $sy2-1) if $focused; }, onSelectCell => sub { my ( $self, $col, $row) = @_; my $index = $row * $ext[0] + $col; return if $index >= @colormap; my $color = sprintf("%06x", $current[$index]); $d-> text("Edit palette, index #$index $color"); }, onClick => sub { my ($self) = @_; my ( $col, $row) = $self-> focusedCell; my $index = $row * $ext[0] + $col; return if $index >= @colormap; $curr_index = $index; $cd-> value( $current[$curr_index]); if ( $cd-> execute == mb::OK) { $current[$curr_index] = $cd-> value; $touch = 1; } else { $current[ $curr_index] = $colormap[ $curr_index]; } $self-> redraw_cell( $col, $row); $image-> colormap( @current); image_reset_display_buffer(); }, ); my $ok = $d-> insert( Button => text => '~OK', origin => [ 15, $fh], modalResult => mb::OK, default => 1, ); $d-> insert( Button => text => 'Cancel', origin => [ $d-> width - $ok-> width - 15, $fh], modalResult => mb::Cancel, ); $cd = Prima::ColorDialog-> new( onChange => sub { my ( $row, $col) = ( int($curr_index / $ext[0]), $curr_index % $ext[0]); $current[ $curr_index ] = $_[0]-> value; $grid-> redraw_cell( $col, $row ); my $color = sprintf("%06x", $current[$curr_index]); $d-> text("Edit palette, index #$curr_index $color"); $touch = 1; $image-> colormap( @current); image_reset_display_buffer(); }, ); my $r = $d-> execute; $d-> destroy; $cd-> destroy; if ( $r == mb::OK) { image_replace( $image) if $touch; } else { $image-> colormap( @colormap); $image-> type($image-> type | im::GrayScale) if $was_grayscale; image_reset_display_buffer(); } } sub slideshow_start { return if $slideshow; $w-> insert( Timer => name => 'SlideshowTimer', timeout => $ini-> {SlideDelay} * 1000, onTick => sub { # same as open_next_image( $w, 'next') but no question asked my ( $basedir, $index, @files) = get_dir_list(); $index = get_next_image_index( 1, \@files) unless defined $index; return slideshow_stop() if $index == $#files; $index++; open_new_image( "$basedir/$files[$index]", slideshow => 1); }, )-> start; $slideshow = 1; update_window_title(); } sub slideshow_stop { return unless $slideshow; $w-> SlideshowTimer-> destroy; $slideshow = undef; update_window_title(); } sub slideshow_toggle { $slideshow ? slideshow_stop : slideshow_start; } sub slideshow_set_delay {{ my $delay = input_box( 'Set slideshow delay', 'In seconds:', $ini-> {SlideDelay}, mb::OkCancel ); return unless defined $delay and length $delay; unless ( $delay =~ /^\d+(\.\d+)?$/) { message("Number required"); redo; } $ini-> {SlideDelay} = $delay; }} sub last_file_add { my $newfile = shift; $newfile = undef if defined($newfile) and not -f $newfile; my @last_files; my @indexes; my $exists; for ( keys %$ini) { next unless /^LastFile(\d+)$/; push @indexes, $_; $last_files[$1] = $ini-> {$_}; $exists = $1 if defined($newfile) && $ini->{$_} eq $newfile; } if ( $exists) { my $d = splice( @last_files, $exists, 1); unshift @last_files, $d; } elsif ( defined $newfile) { push @last_files, $newfile; } @last_files = grep { defined } @last_files; shift @last_files while 5 < @last_files; my $x = $menu-> get_items('lastfile'); if ( $x) { $menu-> remove( $_-> [0]) for @$x; } delete @$ini{@indexes}; my $idx = 1; my @set; for my $file ( @last_files) { $ini-> {'LastFile' . $idx } = $file; push @set, [ "~$idx $file", sub { if ( open_new_image($file)) { last_file_add($filename); } }]; $idx++; } @set = ['lastfileset'] unless @set; $menu-> insert( \@set, 'lastfile', 0); } sub menuitem_toggle { $ini->{$_[1]} = $menu-> toggle( $_[1]) }; sub transition_set { my ( $self, $tx) = @_; $tx =~ s/tx_//; if ( $tx eq 'blend') { return unless loadIPA('Blending transition'); } $menu-> uncheck( 'tx_' . $ini-> {Transition}); $menu-> check( "tx_$tx", $ini-> {Transition} = $tx); } $ini = Prima::IniFile-> create( file => Prima::Utils::path('FotoFix'), default => [ 'Main' => [ AutoBestFit => 0, WindowFit => 0, ImageFit => 0, Path => '.', ChdirPath => '.', SlideDelay => 3, AutoPosition => 0, ShowPartial => 1, Transition => 'block', ], ], )-> section('Main'); $w = Prima::Window-> create( menuItems => [ [ 'file' => '~File' => [ ['open' => '~Open image...' => 'Ctrl+O' => '^O' => \&open_image], ['reopen' => '~Reopen image...'=> 'Ctrl+Shift+O' => '^#O' => \&reopen_image], ['save' => '~Save image' => 'Ctrl+S' => '^S' => \&save_image], ['saveas' => 'S~ave as...' => 'Ctrl+Shift+S' => '^#S'=> \&save_image_as], [], ['first' => '~First image' => 'Home' => kb::Home => \&open_next_image ], ['next' => '~Next image' => 'Space' => kb::Space => \&open_next_image ], ['prev' => '~Previous image' => 'Backspace' => kb::Backspace => \&open_next_image ], ['last' => 'Last image' => 'End' => kb::End => \&open_next_image ], [], [ 'tags' => 'Ta~gs' => [ ['tag' => '~Tag/untag' => 'Ins' => kb::Insert => \&tags_toggle_image ], ['clear' => '~Clear selection' => \&tags_clear ], ['invert' => '~Invert selection' => '*' => '*' => \&tags_invert ], [], ['first_t' => '~First tagged image' => 'Ctrl+Home' => km::Ctrl|kb::Home => \&open_next_tagged_image ], ['next_t' => '~Next tagged image' => 'Ctrl+Space' => km::Ctrl|kb::Space => \&open_next_tagged_image ], ['prev_t' => '~Previous tagged image' => 'Ctrl+Backspace' => km::Ctrl|kb::Backspace => \&open_next_tagged_image ], ['last_t' => '~Last tagged image' => 'Ctrl+End' => km::Ctrl|kb::End => \&open_next_tagged_image ], [], ]], ['tagged' => '~Tagged files' => [['tagset']]], ['lastfile'=> '~Last opened files' => [['lastfileset']]], [], ['fcopy' => 'Copy...' => 'F5' => 'F5' => \&files_copy ], ['fmove' => 'Move...' => 'F6' => 'F6' => \&files_move ], ['prefix' => 'Add prefix...'=>'F7' => 'F7' => \&files_prefix ], ['rename' => 'Rename...' => 'F8' => 'F8' => \&files_rename ], ['delete' => 'Delete...' => 'Del' => kb::Delete => \&files_delete ], ['execute' => 'E~xecute on tagged...' => 'Ctrl+X' => '^X' => \&files_execute ], [], ['E~xit' => 'Esc' => kb::Escape => sub { if ( $magnify) { magnify(0); } else { $::application-> close; } }], ]], ['~Edit' => [ ['copy' => '~Copy' => 'Ctrl+Ins' => km::Ctrl|kb::Insert , sub { $::application-> Clipboard-> image(region_image()); }], ['copybits' => 'Copy as ~displayed' => sub { $::application-> Clipboard-> image(image_as_displayed()); }], ['~Paste' => 'Shift+Ins' => km::Shift|kb::Insert , sub { my $i = $::application-> Clipboard-> image; if ( $i) { $filename = 'Clipboard' unless defined $filename; image_replace( $i); } }], ['-crop' => 'Cr~op' => sub { return unless $image and $region; image_replace( region_image()); }], ['grab' => '~Grab screen...' => \&grab_screen ], [], [ 'convert' => 'Con~vert to'=> [ ['~Monochrome' => sub {image_convert(im::Mono)}], ['~16 colors' => sub {image_convert(im::bpp4)}], ['~256 colors' => sub {image_convert(im::bpp8)}], ['~Grayscale' => sub {image_convert(im::bpp8|im::GrayScale)}], ['~RGB' => sub {image_convert(im::RGB)}], ['~Custom...' => sub {image_convert(0)}], [], ['N' => '~No halftoning' => \&conversion_set], ['O' => '~Ordered' => \&conversion_set], ['E' => '~Error diffusion' => \&conversion_set], ['*P' => 'O~ptimized' => \&conversion_set], ]], [ 'resize' => 'Re~size...' => 'Ctrl+R' => '^R' => \&image_resize, ], [ 'rotate' => '~Rotate and mirror' => [ ['Rotate ~left' => 'Alt+Left' => km::Alt|kb::Left => sub { image_rotate(90) }], ['Rotate ~right' => 'Alt+Right' => km::Alt|kb::Right => sub { image_rotate(270) }], ["Rotate ~180\xB0" => sub { image_rotate(180) }], [], ['Mirror ~vertical' => 'V' => 'v' => sub { image_mirror(1) }], ['Mirror ~horizontal' => 'H' => 'h' => sub { image_mirror(0) }], ]], ['effects' => '~Effects' => [ ['~Invert' => \&image_invert ], ['-redeyes' => '~Remove red eyes' => 'Alt+R' => '@R' => \&image_remove_red_eyes ], ]], ['palette' => 'P~alette' => \&edit_palette ], ]], ['view' => '~View' => [ ['~Zoom' => [ ['~Normal ( 100%)' => 'Z' => 'Z' => sub{zoom_set(1.0)}], [], ['25%' => sub{zoom_set 0.25}], ['~50%' => sub{zoom_set 0.5 }], ['~75%' => sub{zoom_set 0.75}], ['~150%' => sub{zoom_set 1.5 }], ['~200%' => sub{zoom_set 2 }], ['~300%' => sub{zoom_set 3 }], ['~400%' => sub{zoom_set 4 }], ['~600%' => sub{zoom_set 6 }], ['16~00%' =>sub{zoom_set 16 }], [], ['~Increase' => '+' => '+' => sub { zoom_scale 1.1 }], ['~Decrease' => '-' => '-' => sub { zoom_scale 0.9 }], ]], ['F~ull screen' => 'Enter' => kb::Enter => sub { fullscreen( not $fullscreen); }], [ ( $ini->{AutoBestFit} ? '*' : '') . 'AutoBestFit' => 'Fit to ~window' => 'M' => 'm' => \&fitting_set, ],[ ( $ini->{WindowFit} ? '*' : '') . 'WindowFit' => '~Fit to screen' => 'F' => 'f' => \&fitting_set, ],[ ( $ini->{ImageFit} ? '*' : '') . 'ImageFit' => 'Fit to ~image' => 'Ctrl+I' => '^I' => \&fitting_set, ], ['~Minimize' => 'Ctrl+Z' => '^Z' => 'minimize' ], [], [ 'S~caling' => [ $UseBufferedZoom ? ( ['Scaling0' => '~System (unbuffered)' => \&scaling_set ], ['Scaling1' => '~Nearest neighborhood' => \&scaling_set ], ) : ( ['Scaling0' => '~Nearest neighborhood' => \&scaling_set ], ), $UseImageMagick ? ( ['ScalingQuadratic' => 'Bi~linear' => \&scaling_set ], ['ScalingCubic' => 'Bi~cubic' => \&scaling_set ], map { [ "Scaling$_" => $_ => \&scaling_set ] } qw( Triangle Hermite Hanning Hamming Blackman Gaussian Catrom Mitchell Lanczos Bessel Sinc) ) : ( ['Install Prima::Image::Magick for more', sub{}] ) ]], ['animation' => '~Animation' => [ [ '~Start/stop' => 'Ctrl+A' => '^A' => \&animation_toggle ], [ 'Re~wind' => \&animation_rewind ], [ '~Next frame' => 'A' => 'a' => \&animation_next ], ]], ['~Slideshow' => [ ['slideshow' => 'Start/stop ~slideshow' => 'S' => 's' => \&slideshow_toggle ], [ 'Set slideshow ~delay...' => \&slideshow_set_delay ], [], [ 'tx_none', '~No effects' => \&transition_set ], [ 'tx_blend', '~Blend effect' => \&transition_set ], [ 'tx_block', 'B~lock effect' => \&transition_set ], ]], [ ( $ini->{AutoPosition} ? '*' : '') . 'AutoPosition' => 'Change window ~position when resizing' => \&menuitem_toggle ], [ ( $ini->{ShowPartial} ? '*' : '') . 'ShowPartial', 'Show loading ~progress' => \&menuitem_toggle ], ]], [], ['~Help' => [ ["~Information" => "F1" => "F1" => sub { $::application-> open_help($0)}], [], [ "~About" => sub { message < [ [ quit => quit => q => sub {$::application-> close }], ], icon => Prima::StdBitmap::icon(0), visible => 0, onClose => \&on_close, onDestroy => sub { $::application-> destroy }, ); $menu = $w-> menu; $iv = $w-> insert( ImageViewer => size => [ $w-> size], origin => [ 0, 0], growMode => gm::Client, quality => 1, selectable => 1, name => 'IV', zoomPrecision => 1000, valignment => ta::Middle, alignment => ta::Center, onMouseDown => \&iv_mousedown, onMouseUp => \&iv_mouseup, onMouseMove => \&iv_mousemove, onMouseWheel => \&iv_mousewheel, onPaint => \&iv_paint, onSize => \&iv_size, ( $fullscreen_x11 ? ( onKeyDown => \&iv_keydown ) : ()), ); Prima::EventHook::install( sub { fullscreen(0) if $fullscreen_x11; slideshow_stop; }, event => 'Execute', ); $ini-> {Scaling} = $UseBufferedZoom unless exists $ini-> {Scaling} and $menu-> has_item( 'Scaling' . $ini-> {Scaling}); scaling_set( $w, 'Scaling' . $ini-> {Scaling}); $ini-> {Transition} = 'block' if $ini-> {Transition} eq 'blend' and not $UseIPA; transition_set( $w, 'tx_' . $ini-> {Transition}); update_menu_status(); update_menu_tags(); update_window_title(); update_window_size(); if ( @ARGV) { if ( -f $ARGV[0]) { open_new_image( $ARGV[0]); } elsif ( -d $ARGV[0]) { $filename = "$ARGV[0]/."; open_next_image($w, 'first'); } else { message("$ARGV[0] cannot be opened"); } } last_file_add( $filename); $w-> show; $w-> select; # uncomment this for simple benchmarking # open_next_image($w,'next') for 0..20; exit; while ( 1) { eval { run Prima; }; last unless $@; my $err = $@; last if mb::Abort == message_box( 'Fotofix fatal error', $err, mb::Abort|mb::Ignore|mb::Error, { buttons => { mb::Abort => { text => '~Quit' } }}, ); } exit; 1; __DATA__ =pod =head1 NAME FotoFix - simple image viewer =head1 DESCRIPTION FotoFix is a simple image viewer with simple capabilities to take care of freshly downloaded photos from your camera - can walk image lists, rotate images, and remove red eyes (with some luck). It was inspired by IrfanView for Windows, a great but unfortunately non-portable and closed-source product. My experience with various image viewers came to a point where I was no longer satisfied with any, so I wrote yet another one. =head1 INSTALLATION FotoFix requres L, L, and L as dependencies. Whereas the first can be obtained by typing "download perl" in Google, the latter are available from CPAN. =head1 USAGE =head2 Remove red eyes To remove red eyes, select a rectangular area by mouse and do "Edit/Effects/Remove red eyes". This will hopefully eliminate red spots in the given rectangle. If there are false positives, try to reload the image and apply the operation to a smaller area. The algorithm for reducing red eye glow is very simple, so if you have some bad red eyes, not detectable by it, feel free to hack it. =head2 Show pixel value under cursor Press shift and move the mouse around the picture =head2 Magnifying glass Press middle button. To change zoom, rotate the mouse wheel. The mouse pointer gets hidden, but press shift and move the pointer to show it back. =head2 Execute When executing a command for each tagged image, the following substitution rules apply. If C<$_> is found the command, the command is iterated for each tagged file and C<$_> is substituted to the filename. If C<$*> is found, then a single command is executed, where C<$*> is substitled to a list of all tagged files. Both C<$*> and C<$_> cannot be specified simultaneously. If neither is specified, C<$*> is assumed to be appended to the end of the command. =head2 Rename Apply a substitutive perl regular expression to each file, where each filename will be stored in C<$_>, and file index in C<$.>. =head1 BUGS & FEATURES The viewer is very, very simple. If you find a bug, or miss a feature, you are very welcome to hack it as you like, and eventually send me a patch. =head1 LICENSE This software is distributed under BSD license =head1 AUTHOR Dmitry Karasik, Edmitry@karasik.eu.orgE. =cut