#!/usr/bin/perl # Copyright (C) 2008 Eric L. Wilhelm use warnings; use strict; package bin::cropper; use Getopt::Helpful; use File::Fu; sub main { my (@args) = @_; my $size = '640x480'; my $from; my $hopt = Getopt::Helpful->new( usage => 'CALLER [options] ', ['s|size=s', \$size, 'WxH', "crop size ($size)"], ['f|from=s', \$from, 'filename', 'read image list from file'], '+help', ); $hopt->Get_from(\@args); my @files; if($from) { @files = map({chomp; $_} File::Fu->file($from)->read); } else { @files = @args; } my $app = Cropper::App->new; my $f = $app->{frame}; $f->set_crop_size(split(/x/, $size, 2)); $f->set_images(@files); $app->MainLoop; } { ###################################################################### package Cropper::App; use Wx (); use base 'Wx::App'; use Class::Accessor::Classy; ro 'frame'; no Class::Accessor::Classy; sub OnInit { my $self = shift; my $frame = $self->{frame} = Cropper::Frame->new(undef, 'Cropper', size => [450, 350]); $frame->Show(1); return 1; } } # package ############################################################ { ###################################################################### package Cropper::Frame; use warnings; use strict; use Carp; use File::Fu; use wxPerl::Constructors; use base 'wxPerl::Frame'; use Class::Accessor::Classy; lw 'crop_size'; lo 'crop_pos'; ro 'crop_min'; ro 'crop_max'; rw 'crop_zoom'; lo 'xoyo'; ro 'scale'; ro 'image'; lw 'images'; lw 'image_size'; ro 'panel'; ro 'saver'; ro 'text_filename'; no Class::Accessor::Classy; sub new { my $class = shift; my $self = $class->SUPER::new(@_); Wx::BoxSizer->new($self)->Add( $self->{panel} = wxPerl::Panel->new($self) ); 0 and Wx::Event::EVT_KEY_DOWN($self->{panel}, sub { warn "size"; $self->SetSize(Wx::Size->new(800, 600)); $self->Update; warn "done sizing"; }); my $painter; Wx::Event::EVT_MOUSEWHEEL($self->{panel}, sub { my ($s, $e) = @_; my $w = ($e->GetWheelRotation < 0) ? 1 : -1; return if($self->{locked}); my $was = $self->crop_zoom; my $cz = _fit_within($was + $w, $self->crop_min, $self->crop_max); if($was != $cz) { $self->set_crop_zoom($cz); $self->update_crop_pos($e->GetX, $e->GetY); $painter->(); } }); Wx::Event::EVT_MOTION($self->{panel}, sub { my ($s, $e) = @_; my ($x, $y) = ($e->GetX, $e->GetY); $e->Skip; return if($self->{locked}); # warn "motion\n"; if($self->update_crop_pos($x, $y)) { $self->paint; } # else { warn "nopaint"; } }); my $bmp_cache = $self->{bmp_cache} = {}; $painter = sub { my ($s, $e) = @_; $s = $self->{panel}; my ($w, $h) = $s->GetSizeWH; $self->saver->SetSize(10, 0, $w-20, -1); my $bmp = $bmp_cache->{"$w|$h"}; my ($xoff, $yoff); my $scale; unless($bmp) { my $p_prop = $w / $h; my ($img_w, $img_h) = $self->image_size; my $image = $self->image; my $img_prop = $img_w / $img_h; $scale = ($p_prop > $img_prop) ? ($h / $img_h) : ($w / $img_w); $scale = 1 if($scale > 1); my ($sw, $sh) = map({$_ * $scale} $img_w, $img_h); $xoff = ($w - $sw) / 2; $yoff = ($h - $sh) / 2; %$bmp_cache = ( "$w|$h" => $bmp = Wx::Bitmap->new($image->Scale($sw, $sh)), ); $self->{xoyo} = [$xoff, $yoff]; $self->{scale} = $scale; } else { ($xoff, $yoff) = $self->xoyo; $scale = $self->scale; } my $dc = Wx::BufferedPaintDC->newWindow($s); $dc->SetPen(Wx::Pen->new(&Wx::wxGREEN, 3, &Wx::wxLONG_DASH)); $dc->DrawBitmap($bmp, $xoff, $yoff, $dc->Clear ? 1:1); # cursor box { $self->update_crop_pos(map({$_/2} $w, $h)) unless($self->{crop_pos}); my ($cx, $cy) = $self->crop_pos; # warn "$cx, $cy\n"; my ($max, $zoom) = ($self->crop_max, $self->crop_zoom); my ($bw, $bh) = map({$_ * $scale * $max/$zoom} $self->crop_size); my $xo = $cx*$scale + $xoff; my $yo = $cy*$scale + $yoff; $dc->DrawLine($xo, $yo, $xo+$bw, $yo); $dc->DrawLine($xo+$bw, $yo, $xo+$bw, $yo+$bh); $dc->DrawLine($xo+$bw, $yo+$bh, $xo, $yo+$bh); $dc->DrawLine($xo, $yo+$bh, $xo, $yo); } }; $self->{_painter} = $painter; Wx::Event::EVT_PAINT($self, $painter); Wx::Event::EVT_LEFT_DOWN($self->{panel}, sub { my ($s, $e) = @_; return if($self->{locked}); $e->Skip; $self->start_crop_and_save; }); $self->setup_saver; return($self); } sub paint { my $self = shift; $self->{_painter}->(); } my $factor = sub { my @list = sort({$b <=> $a} @_); my $gcf = shift(@list); while(my $next = shift(@list)) { while($next) { my $r = $gcf % $next; $gcf = $next; $next = $r; } } return $gcf; }; sub load_image { my ($self, $file) = @_; my $base = File::Fu->file($file)->basename; $base = lc($base); $base =~ s/[^a-z0-9_.-]/_/g; $base =~ s/_+/_/g; $self->text_filename->SetValue($base); my $image = Wx::Image->new; $image->LoadFile($file, &Wx::wxBITMAP_TYPE_ANY); my ($img_w, $img_h) = ($image->GetWidth, $image->GetHeight); my $img_prop = $img_w / $img_h; { my ($cx, $cy) = $self->crop_size; my $prop = $cx / $cy; $self->{crop_min} = $self->crop_max * ( ($prop > $img_prop) ? ($cx / $img_w) : ($cy / $img_h) ); } $self->set_image_size($img_w, $img_h); $self->{image} = $image; } sub set_images { my ($self, @l) = @_; Wx::InitAllImageHandlers(); $self->SUPER::set_images(@l); $self->load_image(shift(@{$self->{images}})); } sub set_crop_size { my ($self, $x, $y) = @_; my $f = $factor->($x, $y); $self->{crop_max} = $f; $self->{crop_zoom} = $f; $self->SUPER::set_crop_size($x, $y); } sub _fit_within { my ($v, $min, $max) = @_; return $v < $min ? $min : $v > $max ? $max : $v; } sub update_crop_pos { my $self = shift; my ($x, $y) = @_; my $scale = $self->scale or return; # mouse moving before constructed my ($max, $zoom) = ($self->crop_max, $self->crop_zoom); my ($cw, $ch) = map({int($_ * $max / $zoom)} $self->crop_size); my ($iw, $ih) = $self->image_size; my ($xmin, $ymin, $xmax, $ymax) = (0,0, $iw-$cw, $ih-$ch); # scale to image coords and get the upper-left corner my ($xo, $yo) = $self->xoyo; $x = int(($x - $xo) / $scale - $cw/2); $y = int(($y - $yo) / $scale - $ch/2); $x = _fit_within($x, $xmin, $xmax); $y = _fit_within($y, $ymin, $ymax); my $current = $self->{crop_pos} ||= []; if("$x,$y" ne join(',', @$current)) { # warn "update ($x,$y)\n"; return(@$current = ($x, $y)); } else { # warn "no update\n"; return; } } sub setup_saver { my $self = shift; $self->{saver} = my $p = wxPerl::Panel->new($self->{panel}, position => [10, 0]); #my $b_other = wxPerl::Button->new($p, "..."); my $b_ok = wxPerl::Button->new($p, "Ok"); my $b_cancel = wxPerl::Button->new($p, "Cancel"); my $t_file = wxPerl::TextCtrl->new($p, '', style => &Wx::wxTE_PROCESS_ENTER, ); $self->{text_filename} = $t_file; my $s = Wx::BoxSizer->new(&Wx::wxHORIZONTAL); $s->Add($t_file, 1, &Wx::wxEXPAND); $s->Add($b_ok); $s->Add($b_cancel); $p->SetSizer($s); $s->SetSizeHints($p); Wx::Event::EVT_BUTTON($b_ok, -1, sub { $self->finish_crop_and_save; }); Wx::Event::EVT_BUTTON($b_cancel, -1, sub { $self->stop_crop_and_save; }); Wx::Event::EVT_TEXT_ENTER($t_file, -1, sub { $self->finish_crop_and_save; }); #$p->Show(1); $p->Hide; } sub start_crop_and_save { my $self = shift; $self->{locked} = 1; $self->saver->Show(1); $self->text_filename->SetFocus; # try to focus just the part before the extension my $base = $self->text_filename->GetValue; if(my $dotpos = rindex($base, '.')) { $self->text_filename->SetSelection(0, $dotpos); } } sub stop_crop_and_save { my $self = shift; $self->{locked} = 0; $self->saver->Hide; } sub finish_crop_and_save { my $self = shift; my $name = $self->text_filename->GetValue; my ($max, $zoom) = ($self->crop_max, $self->crop_zoom); my ($cx, $cy) = $self->crop_pos; my ($cw, $ch) = map({int($_ * $max / $zoom)} $self->crop_size); my $img = $self->image->GetSubImage( Wx::Rect->new($cx, $cy, $cw, $ch) ); $img->Rescale($self->crop_size); $img->SaveFile($name); if(my $next = shift(@{$self->{images}})) { $self->load_image($next); $self->{locked} = 0; %{$self->{bmp_cache}} = (); $self->saver->Hide; #$self->Refresh; } else { $self->Close; } } } # package ############################################################ package main; if($0 eq __FILE__) { bin::cropper::main(@ARGV); } # vi:ts=2:sw=2:et:sta my $package = 'bin::cropper';