#!/usr/bin/perl

# Copyright (c) 2006 by Jeff Ratcliffe (ra28145 at users dot sourceforge.net)
# This script is released under the GPL license.  Please
# see the included LICENSE file for details.

# To do
# 0. Add status line at bottom with messages like "filename saved"
#    Rewrite in python with python-sane, pytiff & tiff2pdf
#    Add KDE, xfe? menus
#    Add dialog whilst device-dependent options are being parsed
#    Add hidden flag whether a page has been saved
#    On quit or delete if page not saved put up dialog
#    Put windows up in centre of parent
#    Scanning window doesn't have a proper icon.
#    Get scanimage calls down to minimum
#    Fix blocking at:
#      First opening scan window
#      Changing device
#      Importing
#      Rotating
#    Use  $tree_view->window->set_cursor( Gtk2::Gdk::Cursor->new('watch') );
#      and    $tree_view->window->set_cursor (undef); when working.
#    Work out how best to include package.
#    Option to throw up PDF viewer with newly created PDF file
#    Finish website on sourceforge
#    Add donate button to help
#    Add screenshot to help
#    Add icon to homepage for GTKfiles.
#    Make save button default
#    Put in a check that mogrify is available
#    Add tolerance to page sizes
#    Add changelog
#    Add dates to history
#    Sort out sourceforge backups
#    Right click save to PDF or TIFF should default to page range "selected"
#    Make the pixel gap 10 (or some ratio) to prevent scrolling.
#    After deleting a page, should display next in thumbnail list
#    Work out a good place to log error messages
# 1. Progress meter for scans and imports.
# 2. Crop and autocrop
# 3. Look at OCR

# Release procedure:
# 1. make tardist rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf-0....
# 2. cvs update
# 3. cvs tag vx-x-x
# 4. make remote-dist remote-web 
# 5. Upload to sourceforge and release files
# 6. Freshmeat
# 7. Launchpad, upload .pot if necessary
# 8. http://www.gtkfiles.org/app.php/gscan2pdf
# 9. Ubuntu forum

use warnings;
use strict;
use IPC::Open3;
use IO::Handle;
use Gtk2 -init;
use Gtk2::SimpleList;
use Cwd;                             # To obtain current working directory
use File::Basename;                  # Split filename into dir, file, ext
use File::Temp qw(tempfile tempdir); # To create temporary files
use Glib qw(TRUE FALSE);             # To get TRUE and FALSE
use POSIX qw(locale_h);              # To sort out LC_NUMERIC
use Locale::gettext;                 # For translations

my $program = "gscan2pdf";
my $version = "0.8.3";

# Standard paper sizes
my @paper = qw(A4 Letter);
my @x = ( 210, 215.9 );
my @y = ( 297, 279.4 );

# Window parameters
my $border_width = 6;

# Set up domain for gettext (internationalisation)
# Expects /usr/share/locale/xx_XX/LC_MESSAGES/$program.mo
my $d = Locale::gettext->domain($program);

my $test = FALSE;
my $debug = FALSE;
while (defined($ARGV[0])) {
# Set up test mode and make sure file has absolute path and is readable
 if ($ARGV[0] eq "--test") {
  shift @ARGV;
  $test = shift @ARGV;
  $test = getcwd."/$test" if ($test !~ /^\//);
  if (! -r $test) {
   warn $d->get('Error: cannot read'), " '", $test, "'\n"; # xgettext hack
   exit 1;
  }
 }
 elsif ($ARGV[0] eq "--help") {
  system("perldoc $0");
  exit;
 }
 elsif ($ARGV[0] eq "--locale") {
  shift @ARGV;
  if ($ARGV[0] !~ /^\//) {
   $d->dir(getcwd."/".shift @ARGV);
  }
  else {
   $d->dir(shift @ARGV);
  }
 }
 elsif ($ARGV[0] eq "--debug") {
  $debug = TRUE;
  shift @ARGV;
 }
 else {
  warn $d->get('Unknown option'), ' ', shift @ARGV, "\n"; # xgettext hack
  exit 1;
 }
}

if (check_utils()) {
 print "$program ",
  $d->get("requires the libtiff library.\nPlease install it."), "\n";
 exit 1;
}

# Set LC_NUMERIC to C to prevent decimal commas (or anything else) confusing
# scanimage
setlocale(LC_NUMERIC, "C");

# Read config file
my $config = "$ENV{'HOME'}/.$program";
my %SETTING;
if (-r $config) {
 open CONFIG, $config or die $d->get("Can't open config file"). " $config";

 while (<CONFIG>) {
  chomp;			# no newline
  s/#.*//;			# no comments
  s/^\s+//;			# no leading white
  s/\s+$//;			# no trailing white
  next unless length;		# anything left?
  my ($key, $value) = split(/\s*=\s*/, $_, 2);
  $SETTING{$key} = $value;
 }
 close CONFIG;
}

# Define application-wide variables here so that they can be referenced
# in the menu callbacks
my ($windowp, $windowt, $windows, $windowh, $slist);

# Define the menu items for ItemFactory
my @menu_items = ( { path        => '/'.$d->get('_File'),
		     item_type   => '<Branch>' },
		   { path        => '/'.$d->get('_File').'/'.$d->get('_New'),
		     accelerator => '<control>n',
		     callback    => \&new,
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-new'},
		   { path        => '/'.$d->get('_File').'/'.$d->get('_Import TIFF'),
		     accelerator => '<control>i',
		     callback    => \&import,
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-open'},
		   { path        => '/'.$d->get('_File').'/'.$d->get('Sca_n'),
		     callback    => sub { if (defined($windows)) {
                                           $windows -> present;
                                          }
                                          else {
                                           scan_dialog();
                                          } },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-print-preview'},
		   { path        => '/'.$d->get('_File').'/'
		                       .$d->get('_Save PDF'),
		     accelerator => '<control>s',
		     callback    => sub { if (defined($windowp)) {
                                           $windowp -> present;
                                          }
                                          else {
                                           save_PDF();
                                          } },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-save-as'},
		   { path        => '/'.$d->get('_File').'/'
		                       .$d->get('Save _TIFF'),
		     accelerator => '<control>t',
		     callback    => sub { if (defined($windowt)) {
                                           $windowt -> present;
                                          }
                                          else {
                                           save_TIFF();
                                          } },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-save'},
		   { path        => '/'.$d->get('_File').'/sep',
		     item_type   => '<Separator>' },
		   { path        => '/'.$d->get('_File').'/'.$d->get('_Quit'),
		     accelerator => '<control>q',
		     callback    => sub { quit(); Gtk2 -> main_quit; },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-quit' },

		   { path        => '/'.$d->get('_Edit'),
		     item_type   => '<Branch>' },
		   { path        => '/'.$d->get('_Edit').'/'.$d->get('_Delete'),
		     accelerator => 'Delete',
		     callback    => \&delete_pages,
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-delete'},
		   { path        => '/'.$d->get('_Edit').'/'
		                       .$d->get('_Renumber'),
		     accelerator => '<control>r',
		     callback    => sub { renumber($slist, 0, 1, 1); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-sort-ascending' },
		   { path        => '/'.$d->get('_Edit').'/'
		                       .$d->get('Select _All'),
		     accelerator => '<control>a',
		     callback    => \&select_all,
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-select-all' },

		   { path        => '/'.$d->get('_View'),
		     item_type   => '<Branch>' },
		   { path        => '/'.$d->get('_View').'/'
		                       .$d->get('Zoom _100%'),
		     callback    => sub { zoom_button('100%'); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-zoom-100'},
		   { path        => '/'.$d->get('_View').'/'
		                       .$d->get('Zoom to _fit'),
		     callback    => sub { zoom_button('fit'); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-zoom-fit'},
		   { path        => '/'.$d->get('_View').'/'.$d->get('Zoom _in'),
		     accelerator => 'plus',
		     callback    => sub { zoom_button('in'); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-zoom-in'},
		   { path        => '/'.$d->get('_View').'/'.$d->get('Zoom _out'),
		     accelerator => 'minus',
		     callback    => sub { zoom_button('out'); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-zoom-out'},
		   { path        => '/'.$d->get('_View').'/sep',
		     item_type   => '<Separator>' },
		   { path        => '/'.$d->get('_View').'/'
		                       .$d->get('Rotate 90 clockwise'),
		     callback    => sub { rotate(90); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-orientation-landscape'},
		   { path        => '/'.$d->get('_View').'/'
		                       .$d->get('Rotate 180'),
		     callback    => sub { rotate(180); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-orientation-reverse-portrait'},
		   { path        => '/'.$d->get('_View').'/'
                                       .$d->get('Rotate 90 anticlockwise'),
		     callback    => sub { rotate(270); },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-orientation-reverse-landscape'},

		   { path        => '/'.$d->get('_Help'),
		     item_type   => '<LastBranch>' },
		   { path        => '/'.$d->get('_Help').'/'.$d->get('_Help'),
		     accelerator => '<control>h',
		     callback    => sub { if (defined($windowh)) {
                                           $windowh -> present;
                                          }
                                          else {
                                           view_pod();
                                          } },
                     item_type   => '<StockItem>',
                     extra_data  => 'gtk-help' },
		   { path        => '/'.$d->get('_Help').'/'.$d->get('_About'),
		     callback    => \&about } );

# Create the window
my $window = Gtk2::Window -> new;
$window -> set_title ( "$program v$version" );
$window -> signal_connect ( destroy => sub { quit(); Gtk2 -> main_quit; } );

# Note when the window is maximised or not.
$window -> signal_connect(window_state_event => sub {
 my ($w, $event) = @_;
 if ($event -> new_window_state & [ 'maximized' ]) {
  $SETTING{"window_maximize"} = 1;
 }
 else {
  $SETTING{"window_maximize"} = 0;
 }
});

# If defined in the config file, set the window state, size and position
if (defined($SETTING{"window_width"}) and defined($SETTING{"window_height"})) {
 $window -> set_default_size ($SETTING{"window_width"}, $SETTING{"window_height"});
}
else {
 $window -> set_default_size (800, 600);
}
if (defined($SETTING{"window_x"}) and defined($SETTING{"window_y"})) {
 $window -> move ($SETTING{"window_x"}, $SETTING{"window_y"});
}
$window -> maximize
 if (! (defined($SETTING{"window_maximize"}) and ! $SETTING{"window_maximize"}));

# Pass the window a stock icon list
my @icon;
my $i = -1;
foreach my $size (qw(menu small-toolbar large-toolbar button dnd dialog)) {
 ++$i;
 $icon[$i] = $window -> render_icon ('gtk-print-preview', $size) # or gtk-index
}
$window -> set_icon_list ( @icon );

my $main_vbox = new Gtk2::VBox ( FALSE, 1 );
$window -> add ( $main_vbox );

# Create the menu bar
my $menubar = create_menu_bar( $window );
$main_vbox -> pack_start( $menubar, FALSE, TRUE, 0 );

my $tooltips = Gtk2::Tooltips->new;

# Button hbox
my $hboxb = Gtk2::HBox -> new;
$main_vbox -> pack_start ($hboxb, FALSE, FALSE, 0);

foreach (
 [ 'gtk-new',            $d->get('Clears all pages'), \&new ],
 [ 'gtk-open',           $d->get('Import TIFF file'), \&import ],
 [ 'gtk-print-preview',  $d->get('Scan document'), sub { if (defined($windows)) {
                                                 $windows -> present;
                                                }
                                                else {
                                                 scan_dialog();
                                                } } ],
 [ 'gtk-save-as',        $d->get('Save as PDF'), sub { if (defined($windowp)) {
                                               $windowp -> present;
                                              }
                                              else {
                                               save_PDF();
                                              } } ],
 [ 'gtk-save',           $d->get('Save as TIFF'), sub { if (defined($windowt)) {
                                                $windowt -> present;
                                               }
                                               else {
                                                save_TIFF();
                                               } } ],
 [ 'gtk-delete',         $d->get('Delete selected pages'), \&delete_pages ],
 [ 'gtk-sort-ascending', $d->get('Renumber pages from 1 to n'), sub {
                                                 renumber($slist, 0, 1, 1); } ],
 [ 'gtk-select-all',     $d->get('Select all pages'), \&select_all ],
 [ 'gtk-zoom-100',       $d->get('Zoom to 100%'), sub { zoom_button('100%'); } ],
 [ 'gtk-zoom-fit',       $d->get('Zoom to fit'), sub { zoom_button('fit'); } ],
 [ 'gtk-zoom-in',        $d->get('Zoom in'), sub { zoom_button('in'); } ],
 [ 'gtk-zoom-out',       $d->get('Zoom out'), sub { zoom_button('out'); } ],
 [ 'gtk-orientation-landscape', $d->get('Rotate 90 clockwise'), sub {
                                                               rotate(90); } ],
 [ 'gtk-orientation-reverse-portrait', $d->get('Rotate 180'), sub {
                                                               rotate(180); } ],
 [ 'gtk-orientation-reverse-landscape', $d->get('Rotate 90 anticlockwise'),
                                                         sub { rotate(270); } ],
 [ 'gtk-help',           $d->get('Help'), sub { if (defined($windowh)) {
                                           $windowh -> present;
                                          }
                                          else {
                                           view_pod();
                                          } } ],
 [ 'gtk-quit',           $d->get('Quit'), sub { quit(); Gtk2 -> main_quit; } ]
) {
 if (defined(Gtk2::Stock->lookup ($_->[0]))) {
  my $button = Gtk2::Button->new;
  my $image = Gtk2::Image->new_from_stock ($_->[0], 'large-toolbar');
  $button->add ($image);
  $button -> signal_connect(clicked => $_->[2]);
  $hboxb -> pack_start ($button, FALSE, FALSE, 0);
  $tooltips->set_tip ($button, $_->[1]);
 }
}
$tooltips->enable;

# HPaned for thumbnails and detail view
my $hpaned = Gtk2::HPaned -> new;
$main_vbox -> pack_start($hpaned, TRUE, TRUE, 0);

# Thumbnail dimensions
my $widtht = 100;
my $heightt = 100;
if (defined($SETTING{"thumb panel"})) {
 $hpaned -> set_position ($SETTING{"thumb panel"});
}
else {
 $hpaned -> set_position ($widtht);
}

# Scrolled window for thumbnails
my $scwin_thumbs = Gtk2::ScrolledWindow -> new;
$hpaned -> pack1 ($scwin_thumbs, TRUE, TRUE);
$scwin_thumbs -> set_policy ('automatic', 'automatic');

# define filename and i to be hidden
Gtk2::SimpleList -> add_column_type( 'filename',
                                     type => 'Glib::String',
                                     attr => 'hidden' );

# Set up a SimpleList
$slist = Gtk2::SimpleList -> new('#' => 'int',
                                 $d->get('Thumbnails') => 'pixbuf',
                                 'Filename' => 'filename');

# Callback for dropped signal.
$slist -> signal_connect('drag_drop' => sub {
# Block row-changed signal so that the list can be renumbered before the sort
# takes over.
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 return 0;
});

# Now that drag_drop has returned,
# check that the numbering is ascending and renumber if needed.
$slist->signal_connect (drag_end => sub {
 renumber($slist, 0);
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});
 return 0;
});

# Set up callback for right mouse clicks.
$slist->signal_connect(button_press_event => \&handle_clicks);
$slist->signal_connect(button_release_event => \&handle_clicks);

$slist -> get_selection -> set_mode ('multiple');
$slist -> set_headers_visible(FALSE);
$slist -> set_reorderable( TRUE );

# Set the page number to be editable
$slist -> set_column_editable (0, TRUE);

# Set-up the callback when the page number has been edited.
$slist -> {signalid} = $slist -> get_model ->
 signal_connect('row-changed' => sub {
  $slist -> get_model -> signal_handler_block($slist -> {signalid});

# Sort pages
  manual_sort_by_column ($slist, 0);

# And make sure there are no duplicates
  renumber ($slist, 0);
  $slist -> get_model -> signal_handler_unblock($slist -> {signalid});
 });

$scwin_thumbs -> add_with_viewport($slist);

# Scrolled window for detail view
my $scwin_detail = Gtk2::ScrolledWindow -> new;
$hpaned -> pack2 ($scwin_detail, TRUE, TRUE);
$scwin_detail -> set_policy ('automatic', 'automatic');

# Globally define the $image variables for $scwin_detail
my ($image, $eventbox, $scale, $src_pixbuf, $widthi, $heighti);

# Globally define the custom paper box.
my $hboxc;

# Set up call back for list selection to update detail view
$slist -> get_selection -> signal_connect(changed => sub {
 my @page = $slist -> get_selected_indices;

# Remove any image in the detail window
 if (defined($scwin_detail -> child)) {
  my $viewport = $image -> get_parent;
  $image -> destroy if (defined $image);
  $eventbox -> destroy if (defined $eventbox);
  $viewport -> destroy if (defined $viewport);
 }

# Display the new image
 if ($#page > -1) {

# Get dimensions for detail window
  my $widthd = $scwin_detail -> allocation -> width;
  my $heightd = $scwin_detail -> allocation -> height;

  $image = Gtk2::Image -> new;
  $image -> set_from_pixbuf(
           get_pixbuf(scalar($slist->{data}[$page[0]][2]), $heightd, $widthd));

# Need to pack the image in an eventbox to get it to respond to mouse clicks
  $eventbox = Gtk2::EventBox -> new;
  $eventbox -> add ( $image );
  $eventbox->signal_connect(button_press_event => \&handle_clicks);
  $eventbox->signal_connect(button_release_event => \&handle_clicks);

  $scwin_detail -> add_with_viewport($eventbox);
  $scwin_detail -> show_all;
 }
});

# If defined in the config file, set the current directory
$SETTING{"cwd"} = getcwd if (! defined($SETTING{"cwd"}));

# Create a temporary directory for scans
my $dir = tempdir;

# Set up the strings for the possible device-dependent options
my %ddo;
my %pddo = (
             'mode' => { string => $d->get('Mode'),
                         values => {
                                   'Lineart'       => $d->get('Lineart'),
                                   'Grayscale',    => $d->get('Grayscale'),
                                   'Gray'          => $d->get('Gray'),
                                   'Color'         => $d->get('Color'),
                                   'Black & White' => $d->get('Black & White'),
                                   'True Gray'     => $d->get('True Gray'),
                                   'Binary'        => $d->get('Binary'),
                                   'auto'          => $d->get('Auto'),
                                   'Halftone'      => $d->get('Halftone'),
                                   '24bit Color'   => $d->get('24bit Color'),
                                   }
                       },
            'resolution'      => { string => $d->get('Resolution') },
            'batch-scan'      => { string => $d->get('Batch scan'),
                                   values => {
                                             'yes' => $d->get('Yes'),
                                             'no'  => $d->get('No'),
                                             },
                                 },
            'wait-for-button' => { string => $d->get('Wait for button'),
                                   values => {
                                             'yes' => $d->get('Yes'),
                                             'no'  => $d->get('No'),
                                             },
                                 },
            'button-wait'     => { string => $d->get('Wait for button'),
                                   values => {
                                             'yes' => $d->get('Yes'),
                                             'no'  => $d->get('No'),
                                             },
                                 },
            'source'          => { string => $d->get('Source'),
                                   values => {
                                     'Normal'  => $d->get('Normal'),
                                     'ADF'     => $d->get('ADF'),
                                     'Automatic Document Feeder' => $d->get('Automatic Document Feeder'),
                                     'XPA'     => $d->get('XPA'),
                                     'auto'    => $d->get('Auto'),
                                     'Auto'    => $d->get('Auto'),
                                     'Flatbed' => $d->get('Flatbed'),
                                     'Transparency Adapter' => $d->get('Transparency Adapter'),
                                     'Transparency Unit' => $d->get('Transparency Unit'),
                                   },
                                 },
          );

$window -> show_all;
Gtk2 -> main;



### Subroutines

# Create the menu bar, initialize its menus, and return the menu bar.

sub create_menu_bar {
 my ($window) = @_;

 my $accel_group = new Gtk2::AccelGroup;

 # This function initializes the item factory.
 # Param 1: The type of menu - can be 'Gtk2::MenuBar', 'Gtk2::Menu',
 #          or 'Gtk2::OptionMenu'.
 # Param 2: The path of the menu.
 # Param 3: The accelerator group.  The item factory sets up
 #          the accelerator table while generating menus.
 my $item_factory = new Gtk2::ItemFactory( 'Gtk2::MenuBar', '<main>',
                                                                 $accel_group );

 # This function generates the menu items. Pass the item factory,
 # the number of items in the array, the array itself, and any
 # callback data for the the menu items.
 $item_factory -> create_items( @menu_items );

 # Attach the new accelerator group to the window.
 $window -> add_accel_group( $accel_group );

 # Finally, return the actual menu bar created by the item factory.
 #*menubar = gtk_item_factory_get_widget (item_factory, "<main>");
 return ( $item_factory -> get_widget( '<main>' ) );
}


# Zoom the detail window

sub zoom_button {
 my $button = $_[0];
 if (defined($scwin_detail) and defined($scwin_detail -> child)) {
  my $viewport = $image -> get_parent;
  $image -> destroy;
  $eventbox -> destroy;
  $viewport -> destroy;
  if ($button eq '100%') {
   $scale = 1;
  }
  elsif ($button eq 'in') {
   $scale *= 1.2;
  }
  elsif ($button eq 'out') {
   $scale /= 1.2;
  }
  else {
   my $widthd = $scwin_detail -> allocation -> width;
   my $heightd = $scwin_detail -> allocation -> height;
   $scale = ($widthd-1)/$widthi < ($heightd-1)/$heighti
                                  ? ($widthd-1)/$widthi : ($heightd-1)/$heighti;
  }
  my $pixbuf = Gtk2::Gdk::Pixbuf -> new ($src_pixbuf -> get_colorspace,
                                         $src_pixbuf -> get_has_alpha, 
                                         $src_pixbuf -> get_bits_per_sample,
                                         $widthi*$scale, $heighti*$scale);
  zoom_pixbuf($src_pixbuf, $pixbuf, $scale);
  $image -> set_from_pixbuf($pixbuf);

# Need to pack the image in an eventbox to get it to respond to mouse clicks
  $eventbox = Gtk2::EventBox -> new;
  $eventbox -> add ( $image );
  $eventbox->signal_connect(button_press_event => \&handle_clicks);
  $eventbox->signal_connect(button_release_event => \&handle_clicks);

  $scwin_detail -> add_with_viewport($eventbox);
  $scwin_detail -> show_all;
 }
}


# Zoom the image

sub zoom_pixbuf {
 my ($src, $dest, $scale) = @_;
 $src -> composite ($dest, 0, 0, $dest -> get_width,
                                 $dest -> get_height, 
                    0, 0, $scale, $scale, 'bilinear', 255);
}


# Returns the pixbuf scaled to fit in the given box

sub get_pixbuf {
 my ($filename, $height, $width) = @_;
 
# Need the eval otherwise nothing else in the sub gets carried out after an error
# For some reason reading the tiff sometimes throws:
# Failed to load RGB data from TIFF file:
#        libpixbuf-tiff: Read error on strip 134; got 5190 bytes, expected 7130
# Normally succeeds on second attempt

# Change new_from_file for new_from_file_at_scale once I no longer have to
# develop with Gtk2 < 1.090!
 eval { $src_pixbuf = Gtk2::Gdk::Pixbuf -> new_from_file ($filename); };
# if (Glib::Error::matches ($@, 'Mup::Thing::Error', 'flop')) {
#  recover_from_a_flop ();
# }
 if ($@) {
  warn $d->get('Warning: ')."$@\n";
  eval { $src_pixbuf = Gtk2::Gdk::Pixbuf -> new_from_file ($filename); };
  if ($@) {
   return 0;
  }
  else {
   warn $d->get('Information: got')." $filename ".$d->get("on second attempt\n");
  }
 }

# Get dimension for image
 $widthi = $src_pixbuf -> get_width;
 $heighti = $src_pixbuf -> get_height;

# Calculate scale factor
 my $scale = ($width-1)/$widthi < ($height-1)/$heighti
                                   ? ($width-1)/$widthi : ($height-1)/$heighti;

 my $pixbuf = Gtk2::Gdk::Pixbuf -> new ($src_pixbuf -> get_colorspace,
                                        $src_pixbuf -> get_has_alpha, 
                                        $src_pixbuf -> get_bits_per_sample,
                                        $widthi*$scale, $heighti*$scale);
 zoom_pixbuf($src_pixbuf, $pixbuf, $scale);
 return $pixbuf;
}


# Deletes all scans after warning.

sub new {
 my $dialog = Gtk2::MessageDialog -> new ($window, 'destroy-with-parent',
                     'warning', 'ok-cancel',
                     $d->get("This will clear the list of scanned images.\n"
                     . "Do you want to continue?"));
 if ($dialog -> run eq 'ok') {

# Remove any image in the detail window
  if ($image) {
   my $viewport = $image -> get_parent;
   $image -> destroy;
   $viewport -> destroy;
  }

# Depopulate the thumbnail list
  while ($#{$slist -> {data}} > -1) {
   pop @{$slist -> {data}};
  }
 }
 $dialog -> destroy;
}


# Throw up file selector and import selected file

sub import {

# cd back to cwd to get filename
 chdir $SETTING{"cwd"};

 my $file_chooser = Gtk2::FileChooserDialog -> new(
                                      $d->get('Import scan from file'),
                                      $window, 'open',
                                      'gtk-cancel' => 'cancel',
                                      'gtk-ok' => 'ok');
 $file_chooser -> set_select_multiple(TRUE);

 if ('ok' eq $file_chooser->run) {
  my @filename = $file_chooser -> get_filenames;

  foreach my $filename (@filename) {

# Update cwd
   $SETTING{"cwd"} = dirname($filename);

# Check that the file really is a tiff
   my $output = `tiffinfo \"$filename\" 2>&1 1>/dev/null`;
   if ($output =~ /Not a TIFF file/) {
    my $dialog = Gtk2::MessageDialog -> new ($window,
                                            'destroy-with-parent',
                                            'error',
                                            'close',
                                            $d->get('Not a TIFF file'));
    $dialog -> run;
    $dialog -> destroy;
   }
   else {
    my $dialog = Gtk2::Dialog -> new ($d->get('Importing')."...", $window,
                                      'destroy-with-parent',
                                      'gtk-cancel' => 'cancel');

# Set up ProgressBar
    my $pbar = new Gtk2::ProgressBar;
    $pbar -> set_pulse_step(.1);
    $dialog -> vbox -> add($pbar);

# Timer will run until callback returns false 
#    my $timer = Glib::Timeout->add (100, sub {$pbar->pulse; return 1;});
    $pbar->set_fraction(.5);
    $dialog -> show_all;

# cd back to tempdir to import
    chdir $dir;

# Split the tiff into its pages and import them individually
    system("tiffsplit \"$filename\"");
    $pbar->set_fraction(1);
    my @pages = <x???.tif>;
    foreach (@pages) {
     import_scan($_);
    }
    
#    Glib::Source -> remove($timer);
    $dialog -> destroy;
   }

  }
 }

 $file_chooser -> destroy;
}


# Throw up file selector and save selected pages as PDF under given name.

sub save_PDF {

# PDF pop-up window
 $windowp = Gtk2::Window -> new;
 $windowp -> set_border_width($border_width);
 $windowp -> set_title ($d->get('Save as PDF'));
 $windowp -> signal_connect (delete_event => sub {
  $windowp -> hide;
  return TRUE; # ensures that the window is not destroyed
 });
 $windowp -> set_transient_for($window); # Assigns parent

# VBox for window
 my $vbox = Gtk2::VBox -> new;
 $windowp -> add ($vbox);

# Frame for metadata
 my $framem = Gtk2::Frame -> new($d->get('Metadata'));
 $vbox -> pack_start ($framem, TRUE, TRUE, 0);
 my $vboxm = Gtk2::VBox -> new;
 $vboxm -> set_border_width($border_width);
 $framem -> add ($vboxm);

# Date/time
 my $hboxe = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxe, TRUE, TRUE, 0);
 my $labele = Gtk2::Label -> new ($d->get('Date'));
 $hboxe -> pack_start ($labele, FALSE, FALSE, 0);
 if (! (defined($SETTING{'year'}) and defined($SETTING{'month'})
                               and defined($SETTING{'day'}))) {
  ($SETTING{'day'}, $SETTING{'month'}, $SETTING{'year'})
                                                       = (localtime())[3, 4, 5];
  $SETTING{'year'} += 1900;
  $SETTING{'month'} += 1;
 }
 my $button = Gtk2::Button ->
                      new("$SETTING{'year'}/$SETTING{'month'}/$SETTING{'day'}");
 $button -> signal_connect( clicked => sub {
  my $window = Gtk2::Window -> new;
  $window -> set_title ($d->get('Select Date'));
  $window->set_border_width($border_width);
  $window->signal_connect( 'destroy' => sub { $window -> destroy; } );
  $window->set_resizable(FALSE);
  $window -> set_transient_for($windowp); # Assigns parent

  my $vbox = Gtk2::VBox -> new;
  $window -> add($vbox);

  my $calendar = Gtk2::Calendar -> new;
  $calendar -> select_day($SETTING{'day'});
  $calendar -> select_month($SETTING{'month'}-1, $SETTING{'year'});
  $vbox -> pack_start ($calendar, TRUE, TRUE, 0);

  $calendar -> signal_connect(day_selected_double_click => sub {
   ($SETTING{'year'}, $SETTING{'month'}, $SETTING{'day'}) = $calendar ->
                                                                       get_date;
   $SETTING{'month'} += 1;
   $button -> set_label ("$SETTING{'year'}/$SETTING{'month'}/$SETTING{'day'}");
   $window -> destroy;
  });

  $window -> show_all;
 } );
 $tooltips -> set_tip ($button, $d->get('Year/Month/Day'));
 $hboxe -> pack_end( $button, TRUE, TRUE, 0 );

# Document author
 my $hboxa = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxa, TRUE, TRUE, 0);
 my $labela = Gtk2::Label -> new ($d->get('Document author'));
 $hboxa -> pack_start ($labela, FALSE, FALSE, 0);
 my $entrya = Gtk2::Entry -> new;
 $hboxa -> pack_end( $entrya, TRUE, TRUE, 0 );
 $entrya -> set_text($SETTING{"author"}) if (defined($SETTING{"author"}));

# Title
 my $hboxt = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxt, TRUE, TRUE, 0);
 my $labelt = Gtk2::Label -> new ($d->get('Title'));
 $hboxt -> pack_start ($labelt, FALSE, FALSE, 0);
 my $entryt = Gtk2::Entry -> new;
 $hboxt -> pack_end( $entryt, TRUE, TRUE, 0 );
 $entryt -> set_text($SETTING{"title"}) if (defined($SETTING{"title"}));

# Subject
 my $hboxs = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxs, TRUE, TRUE, 0);
 my $labels = Gtk2::Label -> new ($d->get('Subject'));
 $hboxs -> pack_start ($labels, FALSE, FALSE, 0);
 my $entrys = Gtk2::Entry -> new;
 $hboxs -> pack_end( $entrys, TRUE, TRUE, 0 );
 $entrys -> set_text($SETTING{"subject"}) if (defined($SETTING{"subject"}));

# Keywords
 my $hboxk = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxk, TRUE, TRUE, 0);
 my $labelk = Gtk2::Label -> new ($d->get('Keywords'));
 $hboxk -> pack_start ($labelk, FALSE, FALSE, 0);
 my $entryk = Gtk2::Entry -> new;
 $hboxk -> pack_end( $entryk, TRUE, TRUE, 0 );
 $entryk -> set_text($SETTING{"keywords"}) if (defined($SETTING{"keywords"}));

# Get help page to see what compression options are configured in tiff2pdf
 my $output = `tiff2pdf -h 2>&1 1>/dev/null`;
 my @compression;
 push @compression, $d->get('None');
 if ($output =~ /-z/) {
  push @compression, $d->get('Zip');
  $SETTING{'compression'} = 'Zip' if (! defined $SETTING{'compression'});
 }
 push @compression, $d->get('JPEG') if ($output =~ /-j/);

# Compression optionmenu
 my $hboxc = Gtk2::HBox -> new;
 $vbox -> pack_start($hboxc, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Compression'));
 $hboxc -> pack_start ($label, FALSE, FALSE, 0);
 my $optc = Gtk2::OptionMenu -> new;
 my $menuc = Gtk2::Menu -> new;

# Set up quality spinbutton here so that it can be shown or hidden by callback
 my $hboxq = Gtk2::HBox -> new;
 $vbox -> pack_start($hboxq, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('JPEG Quality'));
 $hboxq -> pack_start ($label, FALSE, FALSE, 0);
 my $spinbuttonq = Gtk2::SpinButton -> new_with_range(1, 100, 1);
 if (defined($SETTING{'quality'})) {
  $spinbuttonq->set_value($SETTING{'quality'});
 }
 else {
  $spinbuttonq->set_value(80);
 }
 $hboxq -> pack_end ($spinbuttonq, FALSE, FALSE, 0);

# Fill compression optionmenu
 my $i = 0;
 my $o;
 foreach my $compression (@compression) {
  my $item = Gtk2::MenuItem -> new ($compression);
  $menuc -> append ($item);
  $item -> signal_connect ('activate' => sub {
   if ($compression eq $d->get('JPEG')) {
    $hboxq -> show_all;
   }
   else {
    $hboxq -> hide_all;
   }
  });
  $o = $i
   if (defined($SETTING{'compression'})
       and $compression eq $SETTING{'compression'});
  ++$i;
 }
 $optc -> set_menu ($menuc);
 $optc -> set_history ($o) if defined($o);
 $hboxc -> pack_end ($optc, FALSE, FALSE, 0);

# Frame for page range
 my $framep = Gtk2::Frame -> new($d->get('Page range'));
 $vbox -> pack_start ($framep, TRUE, TRUE, 0);
 my $vboxp = Gtk2::VBox -> new;
 $vboxp -> set_border_width($border_width);
 $framep -> add ($vboxp);

#the first radio button has to set the group,
#which is undef for the first button
# All button
 my $buttona = Gtk2::RadioButton -> new(undef, $d->get('All'));
 $vboxp -> pack_start($buttona, TRUE, TRUE, 0);
 my @group = $buttona -> get_group;

# Current button
 my $buttonc = Gtk2::RadioButton -> new(@group, $d->get('Current'));
 $vboxp -> pack_start($buttonc, TRUE, TRUE, 0);

# Selected button
 my $buttons = Gtk2::RadioButton -> new(@group, $d->get('Selected'));
 $vboxp -> pack_start($buttons, TRUE, TRUE, 0);

# Set default
 if (defined($SETTING{"Page range"})) {
  if ($SETTING{"Page range"} eq "current") {
   $buttonc -> set_active(TRUE);
  }
  elsif ($SETTING{"Page range"} eq "selected") {
   $buttons -> set_active(TRUE);
  }
  else {
   $buttona -> set_active(TRUE);
  }
 }
 else {
  $buttona -> set_active(TRUE);
 }

# Pages button
# my $hboxp = Gtk2::HBox -> new;
# $vboxp -> pack_start($hboxp, TRUE, TRUE, 0);
# my $buttonp = Gtk2::RadioButton -> new(@group, "Pages:");
# $hboxp -> pack_start($buttonp, FALSE, FALSE, 0);

# Page List
# my $entryp = Gtk2::Entry -> new;
# $entryp -> signal_connect ('insert-text' => sub {
#  my ($widget, $string, $len, $position) = @_;
#  if ($string !~ /^\d+$/) { # Only add numbers
#   $entryp -> signal_stop_emission_by_name ('insert-text');
#   $buttonp -> set_active(TRUE); # Set the radiobutton active
#  }
#  () # this callback must return either 2 or 0 items.
# });
# $hboxp -> pack_end ($entryp, FALSE, FALSE, 0);

# HBox for buttons
 my $hboxb = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxb, TRUE, TRUE, 0);

# Save button
 my $sbutton = Gtk2::Button -> new_from_stock('gtk-save');
 $hboxb -> pack_start( $sbutton, TRUE, TRUE, 0 );
 $sbutton -> signal_connect (clicked => sub {

# cd back to cwd to save
  chdir $SETTING{"cwd"};

# Set up file selector
  my $file_chooser = Gtk2::FileChooserDialog -> new($d->get('PDF filename'),
                                                     $windowp, 'save',
                                                     'gtk-cancel' => 'cancel',
                                                     'gtk-save' => 'ok');

  if ('ok' eq $file_chooser->run) {
   my $filename = $file_chooser -> get_filename;
   $filename = $filename.".pdf" if ($filename !~ /.pdf$/i);
   if (-e $filename) {
    my $dialog = Gtk2::MessageDialog -> new ($file_chooser,
     'destroy-with-parent',
     'question', # message type
     'ok-cancel', # which set of buttons?
     $d->get('File') . " $filename " . $d->get("exists.\nReally overwrite?"));
    my $response = $dialog -> run;
    $dialog -> destroy;
    if ($response ne 'ok') {
     $file_chooser -> destroy;
     return 1;
    }
   }

# Update cwd
   $SETTING{"cwd"} = dirname($filename);

# fill $pagelist with filenames depending on which radiobutton is active
   my $n;
   my $pagelist;
   if ($buttona -> get_active) {
    $SETTING{"Page range"} = "all";
    $n = $#{$slist -> {data}};
    $pagelist = $slist -> {data}[0][2];
    my $i = 1;
    while ($i <= $#{$slist -> {data}}) {
     $pagelist = $pagelist." ".$slist -> {data}[$i][2];
     ++$i;
    }
   }
   elsif ($buttonc -> get_active) {
    $SETTING{"Page range"} = "current";
    $n = 1;
    my @page = $slist -> get_selected_indices;
    $pagelist = $slist -> {data}[$page[0]][2];
   }
   elsif ($buttons -> get_active) {
    $SETTING{"Page range"} = "selected";
    my @page = $slist -> get_selected_indices;
    $n = $#page;
    $pagelist = $slist -> {data}[$page[0]][2];
    my $i = 1;
    while ($i <= $#page) {
     $pagelist = $pagelist." ".$slist -> {data}[$page[$i]][2];
     ++$i;
    }
   }

# Get metadata
   $SETTING{"author"} = $entrya -> get_text;
   $SETTING{"title"} = $entryt -> get_text;
   $SETTING{"subject"} = $entrys -> get_text;
   $SETTING{"keywords"} = $entryk -> get_text;

# Get compression
   $SETTING{'compression'} = $compression[$optc -> get_history];
   my $compression = "";
   if ($SETTING{'compression'} eq 'JPEG') {
    $SETTING{'quality'} = $spinbuttonq -> get_value;
    $compression = "-j -q $SETTING{'quality'}";
   }
   elsif ($SETTING{'compression'} eq 'Zip') {
    $compression = "-z";
   }

# Set options
   my $options = " $compression -o \"$filename\""
               . sprintf (" -e %4i%02i%02i", $SETTING{'year'}, $SETTING{'month'}, $SETTING{'day'})
               . " -c \"$program v$version\""
               . " -a \"$SETTING{'author'}\""
               . " -t \"$SETTING{'title'}\""
               . " -s \"$SETTING{'subject'}\""
               . " -k \"$SETTING{'keywords'}\"";

# call tiff2pdf and/or tiffcp according to the number of pages
   if ($n > 0) {
    system ("tiffcp $pagelist \"$filename.tif\"");
    system ("tiff2pdf $options \"$filename.tif\"");
    unlink ("$filename.tif"); # delete the temporary tiff
   }
   else {
    system ("tiff2pdf $options $pagelist");
   }
  
   $windowp -> hide;
  }

  $file_chooser -> destroy;

# cd back to tempdir
  chdir $dir;
 } );

# Cancel button
 my $cbutton = Gtk2::Button -> new_from_stock('gtk-cancel');
 $hboxb -> pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton -> signal_connect( clicked => sub { $windowp -> hide; } );

 $windowp -> show_all;
 $hboxq -> hide_all
  if (! defined($SETTING{'compression'}) or $SETTING{'compression'} ne 'JPEG');
}


# Display page selector and on save a fileselector.

sub save_TIFF {

# PDF pop-up window
 $windowt = Gtk2::Window -> new;
 $windowt -> set_border_width($border_width);
 $windowt -> set_title ($d->get('Save as TIFF'));
 $windowt -> signal_connect (delete_event => sub {
  $windowt -> hide;
  return TRUE; # ensures that the window is not destroyed
 });
 $windowt -> set_transient_for($window); # Assigns parent

# VBox for window
 my $vbox = Gtk2::VBox -> new;
 $windowt -> add ($vbox);

# Frame for page range
 my $framep = Gtk2::Frame -> new($d->get('Page range'));
 $vbox -> pack_start ($framep, TRUE, TRUE, 0);
 my $vboxp = Gtk2::VBox -> new;
 $vboxp -> set_border_width($border_width);
 $framep -> add ($vboxp);

#the first radio button has to set the group,
#which is undef for the first button
# All button
 my $buttona = Gtk2::RadioButton -> new(undef, $d->get('All'));
 $vboxp -> pack_start($buttona, TRUE, TRUE, 0);
 my @group = $buttona -> get_group;

# Current button
 my $buttonc = Gtk2::RadioButton -> new(@group, $d->get('Current'));
 $vboxp -> pack_start($buttonc, TRUE, TRUE, 0);

# Selected button
 my $buttons = Gtk2::RadioButton -> new(@group, $d->get('Selected'));
 $vboxp -> pack_start($buttons, TRUE, TRUE, 0);

# Set default
 if (defined($SETTING{"Page range"})) {
  if ($SETTING{"Page range"} eq "current") {
   $buttonc -> set_active(TRUE);
  }
  elsif ($SETTING{"Page range"} eq "selected") {
   $buttons -> set_active(TRUE);
  }
  else {
   $buttona -> set_active(TRUE);
  }
 }
 else {
  $buttona -> set_active(TRUE);
 }

# HBox for buttons
 my $hboxb = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxb, TRUE, TRUE, 0);

# Save button
 my $sbutton = Gtk2::Button -> new_from_stock('gtk-save');
 $hboxb -> pack_start( $sbutton, TRUE, TRUE, 0 );
 $sbutton -> signal_connect (clicked => sub {

# cd back to cwd to save
  chdir $SETTING{"cwd"};

# Set up file selector
  my $file_chooser = Gtk2::FileChooserDialog -> new($d->get('TIFF filename'),
                                                     $windowt, 'save',
                                                     'gtk-cancel' => 'cancel',
                                                     'gtk-save' => 'ok');

  if ('ok' eq $file_chooser->run) {
   my $filename = $file_chooser -> get_filename;
   if (-e $filename) {
    my $dialog = Gtk2::MessageDialog -> new ($file_chooser,
     'destroy-with-parent',
     'question', # message type
     'ok-cancel', # which set of buttons?
     $d->get('File') . " $filename " . $d->get("exists.\nReally overwrite?"));
    my $response = $dialog -> run;
    $dialog -> destroy;
    return 1 if ($response ne 'ok');
   }

# Update cwd
   $SETTING{"cwd"} = dirname($filename);

# cd back to tempdir
   chdir $dir;

# fill $pagelist with filenames depending on which radiobutton is active
   my $n;
   my $pagelist;
   if ($buttona -> get_active) {
    $SETTING{"Page range"} = "all";
    $n = $#{$slist -> {data}};
    $pagelist = $slist -> {data}[0][2];
    my $i = 1;
    while ($i <= $#{$slist -> {data}}) {
     $pagelist = $pagelist." ".$slist -> {data}[$i][2];
     ++$i;
    }
   }
   elsif ($buttonc -> get_active) {
    $SETTING{"Page range"} = "current";
    $n = 1;
    my @page = $slist -> get_selected_indices;
    $pagelist = $slist -> {data}[$page[0]][2];
   }
   elsif ($buttons -> get_active) {
    $SETTING{"Page range"} = "selected";
    my @page = $slist -> get_selected_indices;
    $n = $#page;
    $pagelist = $slist -> {data}[$page[0]][2];
    my $i = 1;
    while ($i <= $#page) {
     $pagelist = $pagelist." ".$slist -> {data}[$page[$i]][2];
     ++$i;
    }
   }

# Create the tiff
   system ("tiffcp $pagelist \"$filename\"");

   $windowt -> hide;
  }

  $file_chooser -> destroy;
 } );

# Cancel button
 my $cbutton = Gtk2::Button -> new_from_stock('gtk-cancel');
 $hboxb -> pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton -> signal_connect( clicked => sub { $windowt -> hide; } );

 $windowt -> show_all;
}


# Scan

sub scan_dialog {

 my $output = `scanimage --version 2>/dev/null`;
 if ($? ne 0) {
  my $dialog = Gtk2::MessageDialog -> new ($window,
                                          'destroy-with-parent',
                                          'error',
                                          'close',
                                          'scanimage '.$d->get('not found'));
  $dialog -> run;
  $dialog -> destroy;
  return 0;
 }
 my @output = `scanimage --list-devices 2>/dev/null`;
 if ($? eq 0) {
  if ($#output > 1 and $output[0] =~ /^\s$/ 
                   and $output[1] =~ /^No scanners were identified/
                   and ! $test) {
   my $dialog = Gtk2::MessageDialog -> new ($window,
                                           'destroy-with-parent',
                                           'error',
                                           'close',
                                           $d->get('No scanners found'));
   $dialog -> run;
   $dialog -> destroy;
  }
  else {

# scan pop-up window
   $windows = Gtk2::Window -> new;
   $windows -> set_border_width($border_width);
   $windows -> set_title ($d->get('Scan Document'));
   $windows -> set_transient_for($window); # Assigns parent
   $windows -> signal_connect (delete_event => sub {
    $windows -> hide;
    return TRUE; # ensures that the window is not destroyed
   });

# VBox for window
   my $vbox = Gtk2::VBox -> new;
   $windows -> add ($vbox);

# HBox for devices
   my $hboxd = Gtk2::HBox -> new;
   $vbox -> pack_start ($hboxd, FALSE, FALSE, 0);

# device list
   my $labeld = Gtk2::Label -> new ($d->get('Device'));
   $hboxd -> pack_start ($labeld, FALSE, FALSE, 0);

   my $optd = Gtk2::OptionMenu -> new;
   my $menud = Gtk2::Menu -> new;

# Need to define this here to reference it for later.
   my $vboxd = Gtk2::VBox -> new;

# parse out the device and model names
   my @device;
   if (! $test) {
    $output = `scanimage --formatted-device-list="'%i','%d','%v %m'
" 2>/dev/null`;

    my $device = substr($output, 0, index($output, "\n")+1);
    $output = substr($output, index($output, "\n")+1, length($output));
    while ($device =~ /'(\d*)','(.*)','(.*)'/) {
     $device[$1] = $2;

# Convert all underscores to spaces
     (my $model = $3) =~ s/_/ /g;
     $device = substr($output, 0, index($output, "\n")+1);
     $output = substr($output, index($output, "\n")+1, length($output));

# read the device names into the optionmenu
     my $item = Gtk2::MenuItem -> new ($model);
     $item -> signal_connect (activate => sub {
      update_options($vboxd, $device[$optd -> get_history]);
      $vboxd -> show_all;
      if ($#paper > -1 or (defined($SETTING{"Paper size"})
                            and $SETTING{"Paper size"} ne "Custom")) {
# Not defined if scanner has no paper size (like my video grabber)
       $hboxc -> hide if (defined $hboxc);
       $windows -> resize(200, 200); # Doesn't matter that 200x200 is too small
      }
     });
     $menud -> append ($item);
    }
   }
   else {
    my $item = Gtk2::MenuItem -> new (basename($test));
    $item -> signal_connect (activate => sub {
     update_options($vboxd, undef);
     $vboxd -> show_all;
     if ($#paper > -1 or (defined($SETTING{"Paper size"})
                           and $SETTING{"Paper size"} ne "Custom")) {
# Not defined if scanner has no paper size (like my video grabber)
      $hboxc -> hide if (defined $hboxc);
      $windows -> resize(200, 200); # Doesn't matter that 200x200 is too small
     }
    });
    $menud -> append ($item);
    push @device, basename($test);
   }
   $optd -> set_menu ($menud);
   $tooltips -> set_tip ($optd,
                           $d->get('Sets the device to be used for the scan'));
   $hboxd -> pack_end ($optd, FALSE, FALSE, 0);

# If device not set by config and there is a default device, then set it
   if (! defined($SETTING{"device"}) and $output =~ /default device is `(.*)'/) {
    $SETTING{"device"} = $1;
   }

# If device in settings then set it
   if (defined($SETTING{"device"})) {
    my $i = 0;
    ++$i while ($i <= $#device and $SETTING{"device"} ne $device[$i]);
    $optd -> set_history($i) if ($i <= $#device);
   }
   
# Frame for # pages
   my $framen = Gtk2::Frame -> new($d->get('# Pages'));
   $vbox -> pack_start ($framen, FALSE, FALSE, 0);
   my $vboxn = Gtk2::VBox -> new;
   $vboxn -> set_border_width($border_width);
   $framen -> add ($vboxn);

#the first radio button has to set the group,
#which is undef for the first button
# All button
   my $buttona = Gtk2::RadioButton -> new(undef, $d->get('All'));
   $tooltips -> set_tip ($buttona, $d->get('Scan all pages'));
   $vboxn -> pack_start($buttona, TRUE, TRUE, 0);
   my @groupn = $buttona -> get_group;

# Entry button
   my $hboxn = Gtk2::HBox -> new;
   $vboxn -> pack_start($hboxn, TRUE, TRUE, 0);
   my $buttone = Gtk2::RadioButton -> new(@groupn, "#:");
   $tooltips -> set_tip ($buttone, $d->get('Set number of pages to scan'));
   $hboxn -> pack_start($buttone, FALSE, FALSE, 0);

# Number of pages
   my $spin_button = Gtk2::SpinButton -> new_with_range(1, 99, 1);
   $tooltips -> set_tip ($spin_button, $d->get('Set number of pages to scan'));
   $spin_button -> signal_connect ('value-changed' => sub {
    $buttone -> set_active(TRUE); # Set the radiobutton active
   });
   $hboxn -> pack_end ($spin_button, FALSE, FALSE, 0);

# Set default
   if (defined($SETTING{"pages to scan"})) {
    if ($SETTING{"pages to scan"} eq "all") {
     $buttona -> set_active(TRUE);
    }
    else {
     $buttone -> set_active(TRUE);
     $spin_button -> set_value($SETTING{"pages to scan"});
    }
   }
   else {
    $buttone -> set_active(TRUE);
   }

# Frame for source document
   my $frames = Gtk2::Frame -> new($d->get('Source document'));
   $vbox -> pack_start ($frames, FALSE, FALSE, 0);
   my $vboxs = Gtk2::VBox -> new;
   $vboxs -> set_border_width($border_width);
   $frames -> add ($vboxs);

# Single sided button
   my $buttons = Gtk2::RadioButton -> new(undef, $d->get('Single sided'));
   $tooltips -> set_tip ($buttons, $d->get('Source document is single-sided'));
   $vboxs -> pack_start($buttons, TRUE, TRUE, 0);
   $buttons -> set_active(TRUE);
   my @groups = $buttons -> get_group;

# Double sided button
   my $buttond = Gtk2::RadioButton -> new(@groups, $d->get('Double sided'));
   $tooltips -> set_tip ($buttond, $d->get('Source document is double-sided'));
   $vboxs -> pack_start($buttond, FALSE, FALSE, 0);

# Facing/reverse page button
   my $hboxs = Gtk2::HBox -> new;
   $vboxs -> pack_start($hboxs, TRUE, TRUE, 0);
   my $labels = Gtk2::Label -> new ($d->get('Side to scan'));
   $hboxs -> pack_start($labels, TRUE, TRUE, 0);

   my $opts = Gtk2::OptionMenu -> new;
   my $menus = Gtk2::Menu -> new;
   my @side;
   push @side, $d->get('Facing');
   push @side, $d->get('Reverse');
   foreach (@side) {
    my $item = Gtk2::MenuItem -> new ($_);
    $item -> signal_connect (activate => sub {
     $buttond -> set_active(TRUE); # Set the radiobutton active
    });
    $menus -> append ($item);
   }
   $opts -> set_menu ($menus);
   $tooltips -> set_tip ($opts,
              $d->get('Sets which side of a double-sided document is scanned'));
   $hboxs -> pack_end ($opts, FALSE, FALSE, 0);

# Frame for device-dependent options
   my $framed = Gtk2::Frame -> new($d->get('Device-dependent options'));
   $vbox -> pack_start ($framed, FALSE, FALSE, 0);
   $vboxd -> set_border_width($border_width);
   $framed -> add ($vboxd);

# Dig the device-dependent options from --help --device-name
   if ($test) {
    update_options($vboxd, undef);
   }
   else {
    update_options($vboxd, $device[$optd -> get_history]);
   }

# HBox for buttons
   my $hboxb = Gtk2::HBox -> new;
   $vbox -> pack_end ($hboxb, FALSE, FALSE, 0);

# Scan button
   my $sbutton = Gtk2::Button -> new($d->get('Scan'));
   $hboxb -> pack_start( $sbutton, TRUE, TRUE, 0 );
   $sbutton -> signal_connect (clicked => sub {

# Get selected device
    $SETTING{"device"} = $device[$optd -> get_history];
# inverted commas needed for strange characters in device name
    my $device = "--device-name='$SETTING{'device'}'";

# Get device-specific options
    my %options;
    my @child = $vboxd -> get_children;
    foreach my $hbox (@child) {
     my $key;
     if ($hbox -> isa('Gtk2::HBox')) {
      my @child = $hbox -> get_children;
      foreach my $widget (@child) {
       if ($widget -> isa('Gtk2::Label')) {
        $key = get_key($widget -> get_label);
       }
       elsif ($widget -> isa('Gtk2::OptionMenu')) {
        my @child = $widget -> get_children;
        foreach my $widget (@child) {

# ignore artificial paper size option
         $SETTING{$key} = get_value($key, $widget -> get_label);
         if ($widget -> isa('Gtk2::Label') and $key ne 'Paper size') {
          $options{$key} = $SETTING{$key};
         }
        }
       }
       elsif ($widget -> isa('Gtk2::SpinButton')) {
        $options{$key} = $widget -> get_value;
        $SETTING{$key} = $options{$key};
       }
      }
     }
    }

# Get selected number of pages
    my $npages;
    if ($buttone -> get_active) {
     $SETTING{"pages to scan"} = $spin_button -> get_value;
     $npages = "--batch-count=$SETTING{'pages to scan'}";
    }
    else {
     $SETTING{"pages to scan"} = "all";
     $npages = "";
    }

# Start from next available page
    my $start;
    if ($#{$slist -> {data}} > -1) {
     $start = $slist -> {data}[$#{$slist -> {data}}][0] + 1;
    }
    else {
     $start = 1;
    }

# Set step according to single/double sided, facing/reverse page
    my $step;
    if ($buttond -> get_active) {
     if (($opts -> get_history) == 0) { # facing page
      $step = 2;
     }
     else { # reverse page
      if ($start == 1) {
       my $dialog = Gtk2::MessageDialog -> new ($windows,
        'destroy-with-parent',
        'error', # message type
        'cancel', # which set of buttons?
        $d->get('Must scan facing pages first'));
       $dialog -> run;
       $dialog -> destroy;
       return 1;
      }

      $step = -2;

# Check that there is room in the list for the reverse pages
      my $i = 1;
      my $j = $#{$slist -> {data}};
      while ($slist->{data}[$j][0] != $start+$i*$step
             and $start+$i*$step > 0
             and $j > -1) {
       if ($slist->{data}[$j][0] > $start+$i*$step) {
        --$j;
       }
       else {
        ++$i;
       }
      }
      if ($buttone -> get_active) {
       if (($spin_button -> get_value) > $i) {
        my $dialog = Gtk2::MessageDialog -> new ($windows,
         'destroy-with-parent',
         'error', # message type
         'cancel', # which set of buttons?
         $d->get("Cannot scan more reverse pages\nthan facing pages"));
        $dialog -> run;
        $dialog -> destroy;
        return 1;
       }
      }

# If user hasn't specified number of pages, then set number that is undefined
      else {
       $npages = "--batch-count=$i";
      }
     }
     $step = "--batch-increment=$step";
    }
    else {
     $step = "";
    }
    $start = "--batch-start=$start";
    scan($device, $npages, $start, $step, %options);
   } );

# Cancel button
   my $cbutton = Gtk2::Button -> new_from_stock('gtk-close');
#   my $cbutton = Gtk2::Button -> new($d->get('Finished'));
   $hboxb -> pack_end( $cbutton, FALSE, FALSE, 0 );
   $cbutton -> signal_connect( clicked => sub { $windows -> hide; } );

# Show window, hiding the custom hbox and sizing if necessary
   $windows -> show_all;
   if ($#paper > -1 or
       (defined($SETTING{"Paper size"}) and $SETTING{"Paper size"} ne "Custom")) {
# Not defined if scanner has no paper size (like my video grabber)
    $hboxc -> hide if (defined $hboxc);
    $windows -> resize(200, 200); # Doesn't matter that 200x200 is too small
   }
  }
 }
}


# Carry out the scan with the options passed.

sub scan {
 my ($device, $npages, $start, $step, %options) = @_;

# Basic options
 my @options = ( '--format=tiff',  
                 '--batch' );

# Add device-specific options
 my ($key, $value);
 while (($key, $value) = each(%options)) {
  if ($key eq 'x' or $key eq 'y') {
   push @options, "-$key $value";
  }
  else {
   push @options, "--$key='$value'";
  }
 }

# Make sure we are in temp directory
 chdir $dir;

# Create command
 my $cmd = "scanimage $device $npages $start $step @options;echo 'End Scan' > /dev/stderr";
 warn "$cmd\n" if $debug;

 if (! $test) {

# Interface to scanimage
  my ($write, $read);
  my $error = IO::Handle -> new; # this needed because of a bug in open3.
  my $pid = open3($write, $read, $error, $cmd);
  
  my $dialog = Gtk2::Dialog -> new ($d->get('Scanning')."...", $windows,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');
  my $label = Gtk2::Label -> new ($d->get('Scanning')."...");
  $dialog -> vbox -> add ($label);

# Ensure that the dialog box is destroyed when the user responds.
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   local $SIG{HUP} = 'IGNORE';
   kill HUP => $pid;
  });
  $dialog -> show_all;
 
  my $line;
  Glib::IO -> add_watch(fileno($error), 'in', sub {
   my $buffer;
   sysread $error, $buffer, 1024;
   $line = $line . $buffer; # needed to cover case where line is broken in middle
   while ($line =~ /\n/) {
    if ($line =~ /^Scanning (-?\d*) pages/) {
     $label -> set_text($d->get('Scanning')." $1 ".$d->get('pages')."...");
    }
    elsif ($line =~ /^Scanning page (\d*)/) {
     $label -> set_text($d->get('Scanning page')." $1...");
    }
    elsif ($line =~ /^Scanned page (\d*)\. \(scanner status = 5\)/) {

# If the scan can't be loaded then blow the scanning dialog away and
# show an error
     if (! import_scan ("out$1.tif")) {
      $dialog -> destroy;
      $dialog = Gtk2::MessageDialog -> new ($windows, 'destroy-with-parent',
                        'error', 'close', $d->get('Unable to load image'));
      $dialog -> run;
      $dialog -> destroy;
      return 0;
     }
    }
    elsif ($line =~ /^End Scan/) {
     $dialog -> destroy;
     return 0; # Sascha Hunold [hunoldinho at users dot sourceforge dot net]
    }
    elsif ($line =~ /^Scanned page \d*\. \(scanner status = 7\)/) {
     ;
    }
    elsif ($line =~ /^scanimage: sane_start: Document feeder out of documents/) {
     ;
    }
    elsif ($line =~ /^scanimage: rounded/) {
     warn substr($line, 0, index($line, "\n")+1);
    }
    else {
     $dialog -> destroy;
     my $text = $d->get('Unknown message: ')
                                        . substr($line, 0, index($line, "\n"));
warn "$text\n";
     $dialog = Gtk2::MessageDialog -> new ($windows,
                                              'destroy-with-parent',
                                              'error',
                                              'close',
                                              $text);
     $dialog -> run;
     $dialog -> destroy;
    }
    $line = substr($line, index($line, "\n")+1, length($line));
   }
   return 1;
  });
 }
 else {
  warn "$cmd\n";
 }
}


# Take new scan and display it

sub import_scan {
 my ($ofilename) = @_;
 my (undef, $filename) = tempfile(DIR => $dir);
 system("mv $ofilename $filename");

# Add to the page list
 my $i;
 if ($ofilename =~ /out(\d*)\.tif/) {
  $i = $1;
 }
 else {
  $i = $#{$slist -> {data}}+2;
 }

# Block the row-changed signal whilst adding the scan (row) and sorting it.
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 push @{$slist -> {data}},
                     [$i, get_pixbuf($filename, $heightt, $widtht), $filename];
 manual_sort_by_column ($slist, 0);
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Select new page, deselecting others. This fires the select callback,
# displaying the page
 $slist -> get_selection -> unselect_all;
 my @page;

# Due to the sort, must search for new page
 $page[0] = 0;
# $page[0] < $#{$slist -> {data}} needed to prevent infinite loop in case of
# error importing.
 ++$page[0] while ($page[0] < $#{$slist -> {data}}
                    and $slist -> {data}[$page[0]][0] != $i);

 $slist -> select(@page);
 return 1;
}


# Helpers:
sub compare_numeric_col { $_[0] <=> $_[1] }
sub compare_text_col { $_[0] cmp $_[1] }


# Manual one-time sorting of the simplelist's data

sub manual_sort_by_column {
 my ($slist, $sortcol) = @_;

# The sort function depends on the column type
 my %sortfuncs = ( 'Glib::Scalar' => \&compare_text_col,
                   'Glib::String' => \&compare_text_col,
                   'Glib::Int'    => \&compare_numeric_col,
                   'Glib::Double' => \&compare_numeric_col, );

# Remember, this relies on the fact that simplelist keeps model
# and view column indices aligned.
 my $sortfunc = $sortfuncs{$slist->get_model->get_column_type($sortcol)};

# Deep copy the tied data so we can sort it. Otherwise, very bad things happen.
 my @data = map { [ @$_ ] } @{ $slist->{data} };
 @data = sort { $sortfunc->($a->[$sortcol], $b->[$sortcol]) } @data;

 @{$slist->{data}} = @data;
}


# Delete the selected scans

sub delete_pages {
 my @page = $slist -> get_selected_indices;
 while ($#page > -1) {
  splice @{ $slist->{data} }, $page[0], 1;
  @page = $slist -> get_selected_indices;
 }
}


# Select all scans

sub select_all {
 $slist -> get_selection -> select_all;
}


# Display about dialog

sub about {
 my $dialog = Gtk2::MessageDialog -> new ($window, 'destroy-with-parent',
                     'info', 'ok', "$program v$version\n\n"
                     . $d->get('To aid the scan-to-PDF process').".\n\n"
                     . $d->get('Copyright')." Jeff Ratcliffe\n"
                     . $d->get('Licensed under the GPLv2'));
 $dialog -> run;
 $dialog -> destroy;
}


# Check that tiffcp & tiff2pdf exist

sub check_utils {
 system("which tiffcp >/dev/null 2>/dev/null");
 return $? if $? != 0;
 system("which tiff2pdf >/dev/null 2>/dev/null");
 return $? if $? != 0;
 return 0;
}


# Update device-dependent scan options

sub update_options {
 my ($vboxd, $device) = @_;

# Empty $vboxd first
 my @child = $vboxd -> get_children;
 foreach (@child) {
  $_ -> destroy;
 }

 my $output;
 if (defined($device)) {

# Get output from scanimage.
# Inverted commas needed for strange characters in device name
  warn "scanimage --help --device-name='$device'\n" if $debug;
  $output = `scanimage --help --device-name='$device'`;
  warn $output if $debug;
 }
 else {

# Slurp it from file
  $output = do { local( @ARGV, $/ ) = $test ; <> } ;
 }

# Skip to the device-specific options
 $output = substr($output, index($output, "Options specific to device"), length($output));
 $output = substr($output, index($output, "\n")+1, length($output));

# Dig out the paper sizes
 my ($x, $y);
 $x = $2 if ($output =~ /-x (auto\|)?\d\.\.(\d*\.?\d*)/);
 $y = $2 if ($output =~ /-y (auto\|)?\d\.\.(\d*\.?\d*)/);

 if (defined($x) and defined($y)) {

# HBox for paper size
  my $hboxp = Gtk2::HBox -> new;
  $vboxd -> pack_start ($hboxp, FALSE, FALSE, 0);

# Paper list
  my $labelp = Gtk2::Label -> new ($d->get('Paper size'));
  $hboxp -> pack_start ($labelp, FALSE, FALSE, 0);

  my $optp = Gtk2::OptionMenu -> new;
  my $menup = Gtk2::Menu -> new;

# Define custom paper here to reference it in callback
  $hboxc = Gtk2::HBox -> new;
  my $spin_buttonx = Gtk2::SpinButton -> new_with_range(0, $x, 1);
  my $spin_buttony = Gtk2::SpinButton -> new_with_range(0, $y, 1);
  $tooltips -> set_tip ($spin_buttonx, $d->get('Width of scan-area'));
  $tooltips -> set_tip ($spin_buttony, $d->get('Height of scan-area'));

# Add paper size to optionmenu if scanner large enough
# can't use "for" because of the splices
  my $i = 0;
  while ($i <= $#paper) {
   if ($x >= $x[$i] and $y >= $y[$i]) {
    my $item = Gtk2::MenuItem -> new ($paper[$i]);
    $item -> signal_connect (activate => sub {
     my $i = $optp -> get_history;
     $spin_buttonx -> set_value($x[$i]);
     $spin_buttony -> set_value($y[$i]);
     $hboxc -> hide;
     $windows -> resize(200, 200); # Doesn't matter that 200x200 is too small
    });
    $menup -> append ($item);
    ++$i;
   }

# If the paper size isn't possible, remove it from the arrays
   else {
    splice @paper, $i, 1;
    splice @x, $i, 1;
    splice @y, $i, 1;
   }
  }

# Add custom option
  my $item = Gtk2::MenuItem -> new ($d->get('Custom'));
  $item -> signal_connect (activate => sub { $hboxc -> show_all; });
  $menup -> append ($item);
  $optp -> set_menu ($menup);
  $tooltips -> set_tip ($optp,
                $d->get('Selects the paper size, e.g. A4, Letter, or Custom'));
  $hboxp -> pack_end ($optp, FALSE, FALSE, 0);

# Set default paper size from config
  if (defined($SETTING{"Paper size"})) {
   $i = 0;
   while ($i <= $#paper) {
    $optp -> set_history($i) if ($paper[$i] eq $SETTING{"Paper size"});
    ++$i;
   }
  }

# Set scan area from config if available
  if (defined($SETTING{"x"}) and defined($SETTING{"y"})) {
   $spin_buttonx -> set_value($SETTING{"x"});
   $spin_buttony -> set_value($SETTING{"y"});
  }

# Otherwise set from paper size if available
  elsif ($#paper > -1) {
   $i = $optp -> get_history;
   $spin_buttonx -> set_value($x[$i]);
   $spin_buttony -> set_value($y[$i]);
  }

# Or max available
  else {
   $spin_buttonx -> set_value($x);
   $spin_buttony -> set_value($y);
  }

  $vboxd -> pack_start ($hboxc, FALSE, FALSE, 0);

# custom paper x entry
  my $labelx = Gtk2::Label -> new ("x");
  $hboxc -> pack_start ($labelx, FALSE, FALSE, 0);
  $hboxc -> pack_start ($spin_buttonx, FALSE, FALSE, 0);

# custom paper y entry
  my $labely = Gtk2::Label -> new ("y");
  $hboxc -> pack_start ($labely, FALSE, FALSE, 0);
  $hboxc -> pack_start ($spin_buttony, FALSE, FALSE, 0);
 }

# Set device-dependent options
# Dummy entries so that something is returned:
 %ddo = (
          'Paper size' => { string => $d->get('Paper size') },
          'x'          => { string => 'x' },
          'y'          => { string => 'y' },
        );

# Add remaining options
 while ($output =~ /--([a-z_-]*)(.*) \[(.*)\].*\n([\S\s]*)/) {
  my $option = $1;
  my $values = $2;
  my $default = $3;

# Remove everything on the option line and above.
  $output = $4;

# Parse tooltips from option description based on an 8-character indent.
  my $tip = "";
  while ($output =~ /^\s{8,}(.*)\n([\S\s]*)/) {
   if ($tip eq "") {
    $tip = $1;
   }
   else {
    $tip = "$tip $1";
   }

# Remove everything on the description line and above.
   $output = $2;
  }
# batch-scan is HP
# wait-for-button is Epson
# button-wait is HP
# source is various
  if ($option =~ /(^mode$|^resolution$|^batch-scan$|^wait-for-button$|^button-wait$|^source$)/
      and $default !~ /inactive/

# This needed for psc1315, that only has one option for source.
      and ($values =~ /(\d*\.?\d*)\.\.(\d*\.?\d*)/
       or $values =~ /\|/)) {

# Dig out of possible options, if defined
   if (defined($pddo{$option})) {
    $ddo{$option}{string} = $pddo{$option}{string};
   }
   else {
    $ddo{$option}{string} = $option;
   }

# Set default from config
   $default = $SETTING{$option} if (defined($SETTING{$option}));

# HBox for option
   my $hbox = Gtk2::HBox -> new;
   $vboxd -> pack_start ($hbox, TRUE, TRUE, 0);

# Label
   my $label = Gtk2::Label -> new ($ddo{$option}{string});
   $hbox -> pack_start ($label, FALSE, FALSE, 0);

# OptionMenu
   if ($values =~ /\|/) {
    my $opt = Gtk2::OptionMenu -> new;
    my $menu = Gtk2::Menu -> new;

# Counters for defaults
    my $i = -1;
    my $o;

# Parse items from output
    while ($values =~ /\|/) {
     my $value = substr($values, 0, index($values, "|"));
     $values = substr($values, index($values, "|")+1, length($values));
     $value = substr($value, 1, length($value)) while ($value =~ /^ /);
     $value = substr($value, 3, length($value)) while ($value =~ /^\[=\(/);
     add_to_options($option, $value);

# Create item
     my $item = Gtk2::MenuItem -> new ($ddo{$option}{values}{$value});
     $menu -> append ($item);
     $i++;
     $o = $i if ($value eq $default);
    }

# Do it all again for the last one
    $values = substr($values, 0, length($values)-2) while ($values =~ /\)\]/);
    add_to_options($option, $values);
    my $item = Gtk2::MenuItem -> new ($ddo{$option}{values}{$values});
    $menu -> append ($item);
    $i++;
    $o = $i if ($values eq $default);

# Create OptionMenu
    $opt -> set_menu ($menu);
    $opt -> set_history ($o) if defined($o);
    $hbox -> pack_end ($opt, FALSE, FALSE, 0);
    $tooltips -> set_tip ($opt, $tip);
   }

# SpinButton
   elsif ($values =~ /(\d*\.?\d*)\.\.(\d*\.?\d*)/) {
    my $spin_button = Gtk2::SpinButton -> new_with_range($1, $2, 1);
    $spin_button -> set_value($default);
    $hbox -> pack_end ($spin_button, FALSE, FALSE, 0);
    $tooltips -> set_tip ($spin_button, $tip);
   }
  }
 }
}


# Renumber pages

sub renumber {
 my ($slist, $column, $start, $step) = @_;

 $step = 1 if (! defined($step));

 if (defined($start)) {
  for (0 .. $#{$slist -> {data}}) {
   $slist -> {data}[$_][$column] = $_*$step + $start;
  }
 }

# If $start and $step are undefined, just make sure that the numbering is
# ascending.
 else {
  for (0 .. $#{$slist -> {data}}-1) {
   if ($slist -> {data}[$_+1][$column] <= $slist -> {data}[$_][$column]) {

# If at the beginning of the list, start from 1.
    if ($_ == 0) {
     $slist -> {data}[0][$column] = 1;
     $slist -> {data}[1][$column] = 2;
    }
    else {
     $slist -> {data}[$_][$column] = $slist -> {data}[$_-1][$column] + 1;
     $slist -> {data}[$_+1][$column] = $slist -> {data}[$_][$column] + 1
      if ($slist -> {data}[$_+1][$column] <= $slist -> {data}[$_][$column]);
    }
   }
  }
 }
}


# Rotate image

sub rotate {
 my ($degrees) = @_;

 my @page = $slist -> get_selected_indices;
 my $i = 0;
 while ($i <= $#page) {

# Rotate the tiff with imagemagick
  system("mogrify -rotate $degrees ".$slist -> {data}[$page[$i]][2]);

# Use once I no longer have to develop with Gtk2 < 1.090!
#  $slist -> {data}[$page[$i]][1] =
#                  $slist -> {data}[$page[$i]][1] -> rotate_simple ($degrees);
  $slist -> {data}[$page[$i]][1] =
                 get_pixbuf($slist -> {data}[$page[$i]][2], $heightt, $widtht);
  ++$i;
 }

# Reselect the pages to display the rotated image(s)
 $slist->select(@page);
}


# Handle right-clicks

sub handle_clicks {
 my ($widget, $event) = @_;

# let the event chain proceed if not right mouse button
 return FALSE if $event->button != 3;

 my $item_factory = Gtk2::ItemFactory->new("Gtk2::Menu", '<main>', undef);
 my $popup_menu = $item_factory->get_widget('<main>');

 my @menu_items;
 if ($widget->isa('Gtk2::EventBox'))  { # main image
  @menu_items = (
                  { path        => '/'.$d->get('Zoom _100%'),
                    callback    => sub { zoom_button('100%'); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-zoom-100'},
                  { path        => '/'.$d->get('Zoom to _fit'),
                    callback    => sub { zoom_button('fit'); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-zoom-fit'},
                  { path        => '/'.$d->get('Zoom _in'),
                    accelerator => 'plus',
                    callback    => sub { zoom_button('in'); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-zoom-in'},
                  { path        => '/'.$d->get('Zoom _out'),
                    accelerator => 'minus',
                    callback    => sub { zoom_button('out'); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-zoom-out'},
                  { path        => '/sep',
                    item_type   => '<Separator>' },
                  { path        => '/'.$d->get('Rotate 90 clockwise'),
                    callback    => sub { rotate(90); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-landscape'},
                  { path        => '/'.$d->get('Rotate 180'),
                    callback    => sub { rotate(180); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-reverse-portrait'},
                  { path        => '/'.$d->get('Rotate 90 anticlockwise'),
                    callback    => sub { rotate(270); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-reverse-landscape'},
                  { path        => '/sep',
                    item_type   => '<Separator>' },
                  { path        => '/'.$d->get('_Delete'),
                    accelerator => 'Delete',
                    callback    => \&delete_pages,
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-delete'},
  );
 }
 else { # Thumbnail simplelist
  @menu_items = (
                  { path        => '/'.$d->get('_Save PDF'),
                    accelerator => '<control>s',
                    callback    => sub { if (defined($windowp)) {
                                          $windowp -> present;
                                         }
                                         else {
                                          save_PDF();
                                         } },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-save-as'},
                  { path        => '/'.$d->get('Save _TIFF'),
                    accelerator => '<control>t',
                    callback    => sub { if (defined($windowt)) {
                                          $windowt -> present;
                                         }
                                         else {
                                          save_TIFF();
                                         } },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-save'},
                  { path        => '/sep',
                    item_type   => '<Separator>' },
                  { path        => '/'.$d->get('_Renumber'),
                    accelerator => '<control>r',
                    callback    => sub { renumber($slist, 0, 1, 1); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-sort-ascending' },
                  { path        => '/'.$d->get('Select _All'),
                    accelerator => '<control>a',
                    callback    => \&select_all,
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-select-all' },
                  { path        => '/sep',
                    item_type   => '<Separator>' },
                  { path        => '/'.$d->get('Rotate 90 clockwise'),
                    callback    => sub { rotate(90); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-landscape'},
                  { path        => '/'.$d->get('Rotate 180'),
                    callback    => sub { rotate(180); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-reverse-portrait'},
                  { path        => '/'.$d->get('Rotate 90 anticlockwise'),
                    callback    => sub { rotate(270); },
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-orientation-reverse-landscape'},
                  { path        => '/sep',
                    item_type   => '<Separator>' },
                  { path        => '/'.$d->get('_Delete'),
                    accelerator => 'Delete',
                    callback    => \&delete_pages,
                    item_type   => '<StockItem>',
                    extra_data  => 'gtk-delete'},
  );
 }

 $item_factory->create_items(undef, @menu_items);
 $popup_menu->show_all;
 $popup_menu->popup(undef, undef,
                    undef, undef,
                    $event->button,
                    $event->time);

 # block event propagation
 return 1;
}



# Remove temporary files, note window state, save settings and quit.

sub quit {

# Remove temporary files (for some reason File::Temp wasn't doing its job here)
 unlink <$dir/*>;
 rmdir $dir;

# Write window state to settings
 ($SETTING{"window_width"}, $SETTING{"window_height"}) = $window -> get_size;
 ($SETTING{"window_x"}, $SETTING{"window_y"}) = $window -> get_position;
 $SETTING{"thumb panel"} = $hpaned -> get_position;

# Write config file
 open (CONFIG, "> $config")
  or die $d->get("Can't open config file"), " ", $config; # xgettext hack
 while (my ($key, $value) = each %SETTING) {
  print CONFIG "$key = $value\n";
 }
 close CONFIG;
}


# View POD

sub view_pod {
 eval {require Gtk2::Ex::PodViewer;};
 if ($@) {
  my $dialog = Gtk2::MessageDialog -> new ($window,
                                          'destroy-with-parent',
                                          'error',
                                          'close',
    $d->get("The help viewer requires module Gtk2::Ex::PodViewer\n"
                        ."Alternatively, try")."\n$program --help");
  $dialog -> run;
  $dialog -> destroy;
 }
 else {
#  use Gtk2::Ex::PodViewer;
#  use Gtk2::Ex::Simple::List;

# Window
  $windowh = Gtk2::Window -> new;
  $windowh -> set_transient_for($window); # Assigns parent
  $windowh -> signal_connect ( delete_event => sub {
   $windowh -> hide;
   return TRUE; # ensures that the window is not destroyed
  } );
  $windowh -> set_default_size (800, 600);

# Vertical divider between index and viewer
  my $pane = Gtk2::HPaned->new;
  $pane->set_position(200);
  $windowh -> add($pane);

# Index list
  my $index = Gtk2::SimpleList->new('icon' => 'pixbuf',
                                          'title' => 'text',
                                          'link' => 'filename');
  $index->set_headers_visible(FALSE);
  $index->get_column(1)->set_sizing('autosize');

# Index
  my $index_scrwin = Gtk2::ScrolledWindow->new;
  $index_scrwin->set_shadow_type('in');
  $index_scrwin->set_policy('automatic', 'automatic');
  $index_scrwin->add_with_viewport($index);
  $index_scrwin->get_child->set_shadow_type('none');

# Viewer
  my $viewer = Gtk2::Ex::PodViewer->new;
  $viewer->set_border_width($border_width);
  $viewer->set_cursor_visible(FALSE);
  $viewer->signal_connect(link_clicked => \&link_clicked);
  $viewer->signal_connect('link_enter', sub {
   set_status($d->get('Go to')." $_[1]")
  });
  $viewer->signal_connect('link_leave', sub { set_status('') });
  $index->get_selection->signal_connect('changed', sub {
   my $idx = ($index->get_selected_indices)[0];
   my $mark = $index->{data}[$idx][2];
   $viewer->jump_to($mark);
   return 1;
  });

  my $viewer_scrwin = Gtk2::ScrolledWindow->new;
  $viewer_scrwin->set_shadow_type('in');
  $viewer_scrwin->set_policy('automatic', 'automatic');
  $viewer_scrwin->add($viewer);

  $pane->add1($index_scrwin);
  $pane->add2($viewer_scrwin);

  $viewer -> load($0);

# Index contents
  my $idx_pbf = Gtk2::Image->new->render_icon('gtk-jump-to', 'menu');
  map { push(@{$index->{data}}, [ $idx_pbf, strippod ($_), $_ ]) }
                                                             $viewer->get_marks;

  $windowh -> show_all;
 }
}


# Remove formatting characters

sub strippod {
 my $text = shift;
 $text =~ s/B<([^<]*)>/$1/g;
 $text =~ s/E<gt>/>/g;
 $text
}


# Add option, value pair to options

sub add_to_options {
 my ($option, $value) = @_;

# Dig out of possible options, if defined
 if (defined($pddo{$option}) and defined($pddo{$option}{values})
                             and defined($pddo{$option}{values}{$value})) {
  $ddo{$option}{values}{$value} = $pddo{$option}{values}{$value};
 }
 else {
  $ddo{$option}{values}{$value} = $value;
 }
}


# Get option string from label

sub get_key {
 my ($value) = @_;
 foreach my $key ( keys %ddo ) {
  return $key if ($ddo{$key}{string} eq $value);
 }
}


# Get value string from optionmenu

sub get_value {
 my ($option, $value) = @_;
 foreach my $key ( keys %{$ddo{$option}{values}} ) {
  return $key if ($ddo{$option}{values}{$key} eq $value);
 }
}

__END__

=head1 Name

gscan2pdf - A GUI to ease the process of producing a multipage PDF from a scan. gscan2pdf should work on almost any Linux/BSD machine.

=head1 Synopsis

=over

=item 1. Scan one or several pages in with File/Scan

=item 2. Create PDF of selected pages with File/Save PDF

=back

=head1 Description

At maturity, the GUI will have similar features to that of the Windows Imaging program, but with the express objective of writing a PDF, including metadata.

Scanning is handled with SANE via scanimage.
PDF conversion is done by libtiff.

Perl is used for portability and ease of programming, with gtk2-perl for the GUI. This should therefore work more or less out of the box on any system with Perl, gtk2-perl, scanimage and libtiff.

=head1 Download

gscan2pdf is available on Sourceforge.

=head2 Debian-based

If you are using a Debian-based system, just add the following line to your "F</etc/apt/sources.list>" file:

C<deb http://gscan2pdf.sourceforge.net/download/debian binary/>

If you are you are using Synaptic, then use menu I<Edit/Reload Package Information>, search for gscan2pdf in the package list, and lo and behold, you can install the nice shiny new version automatically.

From the command line:

C<apt-get update>

C<apt-get install gscan2pdf>

The help will require F<Gtk2::Ex::PodViewer>. Rotating requires F<Imagemagick>. Having installed gscan2pdf, locate the gscan2pdf entry in Synaptic, right-click it and you can install them under I<Recommends>.

=head2 Other packaging systems

At the moment, the source is hosted in the files section of the gscan2pdf project on Sourceforge (L<http://sourceforge.net/project/showfiles.php?group_id=174140>).

Having downloaded it, C<make install> will install it for you. Note there is no C<make> or C<make test> step.

If you would like me to create RPMs, or packages from some other system, and you are willing to test it while I set it up, send me an email.

=head1 Reporting bugs

Before reporting bugs, please read the L</"FAQs"> section.

Please report any bugs found to the bugs page of the gscan2pdf project on Sourceforge (L<https://sourceforge.net/tracker/?group_id=174140&atid=868098>).

Please include the output of C<scanimage --help> with any new bug report.

=head1 Translations

I intend to produce a multi-lingual version of gscan2pdf in the near future. At that point, I would be glad of help translating the application.

=head1 Menus

=head2 File

=head3 New

Clears the page list.

=head3 Import

Imports a single or multipage TIFF.

=head3 Scan

Sets options before scanning via SANE.

=head4 Device

Chooses between available scanners.

=head4 # Pages

Selects the number of pages, or all pages to scan.

=head4 Source document

Selects between single sided or double sides pages.

This affects the page numbering. Single sided scans are numbered consecutively. Double sided scans are incremented (or decremented, see below) by 2, i.e. 1, 3, 5, etc..

=head4 Side to scan

If double sided is selected above, assuming a non-duplex scanner, i.e. a scanner that cannot automatically scan both sides of a page, this determines whether the page number is incremented or decremented by 2.

To scan both sides of three pages, i.e. 6 sides:

=over

=item 1. Select:

# Pages = 3 (or "all" if your scanner can detect when it is out of paper)

Double sided

Facing side

=item 2. Scans sides 1, 3 & 5.

=item 3. Put pile back with scanner ready to scan back of last page.

=item 4. Select:

# Pages = 3 (or "all" if your scanner can detect when it is out of paper)

Double sided

Reverse side

=item 5. Scans sides 6, 4 & 2.

=item 6. gscan2pdf automatically sorts the pages so that they appear in the correct order.

=back

=head4 Device-dependent options

These, naturally, depend on your scanner. They can include

=over

=item Page size.

=item Mode (colour/black & white/greyscale)

=item Resolution (in dpi)

=item Batch-scan

Guarantees that a "no documents" condition will be returned after the last scanned page, to prevent endless flatbed scans after a batch scan.

=item Wait-for-button/Button-wait

After sending the scan command, wait until the button on the scanner is pressed before actually starting the scan process.

=item Source

Selects the document source. Possible options can include Flatbed or ADF. On some scanners, this is the only way of generating an out-of-documents signal.

=back

=head3 Save PDF

Saves the current, selected or all pages as a PDF.

=head4 Metadata

Metadata are information that are not visible when viewing the PDF, but are embedded in the file and so searchable and can be examined, typically with the "Properties" option of the PDF viewer.

The metadata are completely optional.

=head3 Save TIFF

Saves the current, selected or all pages as a TIFF.

=head2 Edit

=head3 Delete

Deletes the selected page.

=head3 Renumber

Renumbers the pages from 1..n.

Note that the page order can also be changed by drag and drop in the thumbnail view.

=head3 Select All

Selects all pages.

=head2 View

=head3 Zoom 100%

Zooms to 1:1. How this appears depends on the desktop resolution.

=head3 Zoom to fit

Scales the view such that all the page is visible.

=head3 Zoom in

=head3 Zoom out

=head3 Rotate 90 clockwise

=head3 Rotate 180

=head3 Rotate 90 anticlockwise

=head1 FAQs

=head2 Why isn't option xyz available in the scan window?

Possibly because SANE or your scanner doesn't support it.

If an option listed in the output of C<scanimage --help> that you would like to use isn't available, send me the output and I will look at implementing it.

=head2 I've only got an old flatbed scanner with no automatic sheetfeeder. How do I scan a multipage document?

If you are lucky, you have an option like Wait-for-button or Button-wait, where the scanner will wait for you to press the scan button on the device before it starts the scan, allowing you to scan multiple pages without touching the computer.

Otherwise, you have to set the number of pages to scan to 1 and hit the scan button on the scan window for each page.

=head1 See Also

Requires perl, libgtk2-perl (>=1.100-1), libglib-perl (>= 1.100-1), scanimage
and libtiff.

=head1 Author

Jeffrey Ratcliffe (ra28145 at users dot sourceforge dot net)

=head1 Thanks to

All the people who have sent patches, bugs and feedback. Also to Sourceforge for hosting the project.

=for html <a href="http://sourceforge.net"><img alt="SourceForge.net Logo" align="right" height="31px" width="88px"
src="http://sourceforge.net/sflogo.php?group_id=174140&amp;type=1"></a>

=cut
