#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

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

# To do
#    sort out warning when page dropped on, say, detail view.
#    try a document with 250 pages
#    cope with or prevent pages being deleted during scan/unpaper/ocr
#    write hg2cl if necessary
#    simplify code - only keep variables that are needed
#    - rename those like $hboxu to $hbox
#    get it to work on the TV tuner
#    check out scanbuttond
#    for scan profiles, switch to Config::General (libconfig-general-perl)
#     then put paper sizes in config
#    rotating doesn't update detail view - sometimes (wait problem?)
#    problem with wait after cancelling scan?
#    cover case where unpaper or ocr starts running on a page that has in the
#     meantime been deleted
#    use -x option (first column 0-100) in gocr to give more detail to progressbar
#    add run script option
#    check out freeze when gocr does not return properly
#    add ocrad, ocre support
#    Add status line at bottom with messages like "filename saved"
#    Add KDE, Xfce menus
#    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
#    Fix blocking at: Saving tiff, Rotating
#    Use  $tree_view->window->set_cursor( Gtk2::Gdk::Cursor->new('watch') );
#      and    $tree_view->window->set_cursor (undef); when working.
#    Option to throw up PDF viewer with newly created PDF file
#    Translate documentation
#    Right click save to PDF or TIFF should default to page range "selected"
#    Add "translate this application" to help menu like gedit, opening launchpad in the default browswer.
#    Switch viewing widget as soon as packages are available for:
#     http://giv.sourceforge.net/gtk-image-viewer/
#    As soon as gtk+ 2.12 is available and released in Gutsy, remove the
#     EventBox wrappers from the ComboBoxes (check that the tooltips still work!)
#
# From schmolch
# 1.) If the scanning process gets interrupted the automatic numbering screws up.
#      It would be very helpful to be able to tell the number at which it is supposed
#      to continue counting and into which direction (reverse counting stops working
#      after the interruption).
#
# 1. Progress meter for scans.
# 2. Crop and autocrop
#     (see http://mail.gnome.org/archives/gtk-perl-list/2006-June/msg00002.html
#       and ../../Gtk2-Perl-Demo-0.04/examples/scribble.pl)
#
# Release procedure:
# 0. Test scan in lineart, greyscale and colour.
# 1. New screendump required? Print screen creates screenshot.png in Desktop.
#    Make sure that file list in .spec and hg manifest reflects MANIFEST
#    Download new translations
#    Update translators in credits
#    Check a translation with LANGUAGE=de gscan2pdf --debug
#    Make sure that changelog reflects History and hg log from bin/gscan2pdf
# 2. perl Makefile.PL
#    make rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf-x.x.x_all.deb
# 3. hg status
#    hg tag vx-x-x
#    hg push ssh://ra28145@shell.sf.net//home/groups/g/gs/gscan2pdf/hg/gscan2pdf
# 4. make remote-dist remote-html 
# 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
#    gscan2pdf-announce

use warnings;
use strict;
use Gscan2pdf;
use Gtk2 -init;
use Gtk2::Ex::Simple::List;
use Gtk2::Gdk::Keysyms;
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 Socket;
use FileHandle;
use Image::Magick;

# To sort out LC_NUMERIC and $SIG{CHLD}
use POSIX qw(locale_h :signal_h :errno_h :sys_wait_h);
use Locale::gettext 1.05;            # For translations

my $program = 'gscan2pdf';
my $version = '0.9.16';

# Standard paper sizes
my @paperg = ('A4', 'US Letter', 'US Legal', 'Custom');
my @xg = (210, 216, 216, 0);
my @yg = (297, 279, 356, 0);
my (@paper, @x, @y);
my $tolerance = 1;

# Window parameters
my $border_width = 6;

# Image border to ensure that a scaled to fit image gets no scrollbars
my $border = 1;

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

my @test;
my @device;
my @model;
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;
  my $test = shift @ARGV;
  if ($test =~ /(.*)=(.*)/) {
   push @device, $2;
   $test = $1;
   $test = getcwd."/$test" if ($test !~ /^\//);
   if (! -r $test) {
    warn sprintf($d->get("Error: cannot read file: %s\n"), $test);
    exit 1;
   }
   push @test, $test;

# Convert all underscores to spaces
   $test =~ s/_/ /g;
   push @model, basename($test);
  }
  else {
   warn sprintf($d->get("Usage:\n%s --test <file>=<model>\n"), $0); # better 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;
  warn "$program $version\n";
 }
 else {
  warn sprintf($d->get("Unknown option %s.\n"), shift @ARGV); # xgettext hack
  exit 1;
 }
}

# Set LC_NUMERIC to C to prevent decimal commas (or anything else) confusing
# scanimage
setlocale(LC_NUMERIC, "C");
warn "Using ", setlocale( LC_CTYPE ), " locale\n" if ($debug);

# Read config file
my $config = "$ENV{'HOME'}/.$program";

# Set some defaults
my %SETTING = (
 'window_width'    => 800,
 'window_height'   => 600,
 'window_maximize' => TRUE,
 'thumb panel'     => 100,
 'Page range'      => 'all',
 'l'               => 0,
 't'               => 0,
 'layout'          => 'single',
 'downsample'      => FALSE,
 'downsample dpi'  => 150,
);

if (-r $config) {
 open CONFIG, $config
  or die sprintf($d->get("Can't open config file: %s\n"), $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;
}

if ($debug) {
 print "Gtk2-Perl version $Gtk2::VERSION\n";
 print "Built for ".join(".",Gtk2->GET_VERSION_INFO)."\n";
 print "Running with ".join(".",Gtk2->get_version_info)."\n";

 use Data::Dumper;
 print Dumper(\%SETTING);
}

# Just in case dependencies have changed, put put startup warning again
$SETTING{'startup warning'} = TRUE
 if (! defined($SETTING{version}) or $SETTING{version} ne $version);
$SETTING{version} = $version;

# Create icons for rotate buttons
my $IconFactory = undef;
my $path;
if (-d '/usr/share/gscan2pdf') {
 $path = '/usr/share/gscan2pdf';
}
else {
 $path = '.'; # make this a big cleverer, going one dir down from bin.
}
init_icons(
 [ 'rotate90',  "$path/stock-rotate-90.svg" ], 
 [ 'rotate180', "$path/stock-rotate-180.svg" ], 
 [ 'rotate270', "$path/stock-rotate-270.svg" ],
 [ 'scanner',	"$path/scanner.png" ],
 [ 'pdf',	"$path/pdf.png" ],
);

# Define application-wide variables here so that they can be referenced
# in the menu callbacks
my ($windowp, $windowi, $windowv, $windowe, $windows, $windowh, $windowo,
    $windowu, $slist, $vboxd, $combobd, $combobp, $hboxc, @undo_buffer,
    @redo_buffer, @undo_selection, @redo_selection, %dependencies,
    @ocr_stack, $ocr_timer, %helperTag, @unpaper_stack, $unpaper_timer,
    $scanwatch, @ocr_engine, $bscanall, $bscannum,

# Variables for inter-process communication
    $reader, $writer);

my @action_items = (
 # Fields for each action item:
 # [name, stock_id, value, label, accelerator, tooltip, callback]
 
 # File menu
 [ 'File', undef, $d->get('_File') ],
 [ 'New', 'gtk-new', $d->get('_New'), '<control>n', $d->get('Clears all pages'), \&new ],
 [ 'Import', 'gtk-open', $d->get('_Import'), '<control>i', $d->get('Import image file(s)'), \&import ],
 [ 'Scan', 'scanner', $d->get('S_can'), undef, $d->get('Scan document'), \&scan_dialog ],
 [ 'Save PDF', 'pdf', $d->get('_Save PDF'), '<shift><control>s', $d->get('Save as PDF'), \&save_PDF_dialog ],
 [ 'Save image', 'gtk-save', $d->get('Save _Image'), '<control>s', $d->get('Save image'), \&save_image_dialog ],
 [ 'Save DjVu', undef, $d->get('Save _DjVu'), undef, $d->get('Save as DjVu'), \&save_djvu_dialog ],
 [ 'Email as PDF', 'gtk-edit', $d->get('_Email as PDF'), '<control>e', $d->get('Attach as PDF to a new email'), \&email ],
 [ 'Quit', 'gtk-quit', $d->get('_Quit'), '<control>q', $d->get('Quit'), sub { quit(); Gtk2 -> main_quit; } ],
 
 # Edit menu
 [ 'Edit', undef, $d->get('_Edit') ],
 [ 'Undo', 'gtk-undo', $d->get('_Undo'), '<control>z', $d->get('Undo'), \&undo ],
 [ 'Redo', 'gtk-redo', $d->get('_Redo'), '<shift><control>z', $d->get('Redo'), \&unundo ],
 [ 'Delete', 'gtk-delete', $d->get('_Delete'), undef, $d->get('Delete selected pages'), \&delete_pages ],
 [ 'Renumber', 'gtk-sort-ascending', $d->get('_Renumber'), '<control>r', $d->get('Renumber pages from 1 to n'), sub { renumber($slist, 0, 1, 1); } ],
 [ 'Select All', 'gtk-select-all', $d->get('Select _All'), '<control>a', $d->get('Select all pages'), \&select_all ],
 [ 'Frontend', undef, $d->get('_Frontend') ],
 
 # View menu
 [ 'View', undef, $d->get('_View') ],
 [ 'Zoom 100', 'gtk-zoom-100', $d->get('Zoom _100%'), undef, $d->get('Zoom to 100%'), sub { zoom_button('100%'); } ],
 [ 'Zoom to fit', 'gtk-zoom-fit', $d->get('Zoom to _fit'), undef, $d->get('Zoom to fit'), sub { zoom_button('fit'); } ],
 [ 'Zoom in', 'gtk-zoom-in', $d->get('Zoom _in'), 'plus', $d->get('Zoom in'), sub { zoom_button('in'); } ],
 [ 'Zoom out', 'gtk-zoom-out', $d->get('Zoom _out'), 'minus', $d->get('Zoom out'), sub { zoom_button('out'); } ],
 [ 'Rotate 90', 'rotate90', $d->get('Rotate 90 clockwise'), undef, $d->get('Rotate 90 clockwise'), sub { rotate(90); } ],
 [ 'Rotate 180', 'rotate180', $d->get('Rotate 180'), undef, $d->get('Rotate 180'), sub { rotate(180); } ],
 [ 'Rotate 270', 'rotate270', $d->get('Rotate 90 anticlockwise'), undef, $d->get('Rotate 90 anticlockwise'), sub { rotate(270); } ],
 
 # Tools menu
 [ 'Tools', undef, $d->get('_Tools') ],
 [ 'unpaper', undef, $d->get('_unpaper'), undef, $d->get('Clean up current page with unpaper'), \&unpaper ],
 [ 'OCR', undef, $d->get('_OCR'), undef, $d->get('Optical Character Recognition of current page'), \&OCR ],

 # Help menu
 [ 'Help menu', undef, $d->get('_Help') ],
 [ 'Help', 'gtk-help', $d->get('_Help'), '<control>h', $d->get('Help'), \&view_pod ],
 [ 'About', 'gtk-about', $d->get('_About'), undef, $d->get('_About'), \&about ],
);

my @frontends = (
  #Fields for each radio-action item:
  #[name, stock_id, value, label, accelerator, tooltip, value]

 [ 'scanimage', undef, 'scan_image', undef, 'scanimage', 0 ],
 [ 'scanadf',  undef, 'scan_adf', undef, 'scanadf', 1 ],
);

# Declare the XML structure
my $uimanager;
my $ui = "<ui>
 <menubar name='MenuBar'>
  <menu action='File'> 
   <menuitem action='New'/>
   <menuitem action='Import'/>
   <menuitem action='Scan'/>
   <menuitem action='Save PDF'/>
   <menuitem action='Save image'/>
   <menuitem action='Save DjVu'/>
   <menuitem action='Email as PDF'/>
   <separator/>
   <menuitem action='Quit'/>
  </menu>
  <menu action='Edit'> 
   <menuitem action='Undo'/>
   <menuitem action='Redo'/>
   <separator/>
   <menuitem action='Delete'/>
   <menuitem action='Renumber'/>
   <menuitem action='Select All'/>
   <separator/>
   <menu action='Frontend'>
    <menuitem action='scanimage'/>
    <menuitem action='scanadf'/>
   </menu>
   <separator/>
   <menuitem action='Options'/>
   <menuitem action='RestoreWindow'/>
  </menu>
  <menu action='View'> 
   <menuitem action='Zoom 100'/>
   <menuitem action='Zoom to fit'/>
   <menuitem action='Zoom in'/>
   <menuitem action='Zoom out'/>
   <separator/>
   <menuitem action='Rotate 90'/>
   <menuitem action='Rotate 180'/>
   <menuitem action='Rotate 270'/>
  </menu>
  <menu action='Tools'> 
   <menuitem action='unpaper'/>
   <menuitem action='OCR'/>
  </menu>
  <menu action='Help menu'> 
   <menuitem action='Help'/>
   <menuitem action='About'/>
  </menu>
 </menubar>
 <toolbar name='ToolBar'>
  <toolitem action='New'/>
  <toolitem action='Import'/>
  <toolitem action='Scan'/>
  <toolitem action='Save PDF'/>
  <toolitem action='Save image'/>
  <toolitem action='Email as PDF'/>
  <separator/>
  <toolitem action='Undo'/>
  <toolitem action='Redo'/>
  <separator/>
  <toolitem action='Delete'/>
  <toolitem action='Renumber'/>
  <toolitem action='Select All'/>
  <separator/>
  <toolitem action='Zoom 100'/>
  <toolitem action='Zoom to fit'/>
  <toolitem action='Zoom in'/>
  <toolitem action='Zoom out'/>
  <separator/>
  <toolitem action='Rotate 90'/>
  <toolitem action='Rotate 180'/>
  <toolitem action='Rotate 270'/>
  <separator/>
  <toolitem action='Help'/>
  <toolitem action='Quit'/>
 </toolbar>
 <popup name='Detail_Popup'>
  <menuitem action='Zoom 100'/>
  <menuitem action='Zoom to fit'/>
  <menuitem action='Zoom in'/>
  <menuitem action='Zoom out'/>
  <separator/>
  <menuitem action='Rotate 90'/>
  <menuitem action='Rotate 180'/>
  <menuitem action='Rotate 270'/>
  <separator/>
  <menuitem action='Delete'/>
 </popup>
 <popup name='Thumb_Popup'>
  <menuitem action='Save PDF'/>
  <menuitem action='Save image'/>
  <menuitem action='Save DjVu'/>
  <menuitem action='Email as PDF'/>
  <separator/>
  <menuitem action='Renumber'/>
  <menuitem action='Select All'/>
  <separator/>
  <menuitem action='Rotate 90'/>
  <menuitem action='Rotate 180'/>
  <menuitem action='Rotate 270'/>
  <separator/>
  <menuitem action='Delete'/>
 </popup>
</ui>";

# Create the window
my $window = Gtk2::Window -> new;
$window -> set_title ( "$program v$version" );
$window -> signal_connect ( 'delete-event' => 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'} = TRUE;
 }
 else {
  $SETTING{'window_maximize'} = FALSE;
 }
});

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

$window -> set_default_icon_from_file ("$path/gscan2pdf.png");

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

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

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

# HPaned for thumbnails and detail view
my $hpaned = Gtk2::HPaned -> new;
$hpaned -> set_position ($SETTING{'thumb panel'});
$vbox -> pack_start($hpaned, TRUE, TRUE, 0);

# Thumbnail dimensions
my $widtht = 100;
my $heightt = 100;

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

# define hidden string column for filename and annotation buffer
Gtk2::Ex::Simple::List -> add_column_type( 'hstring',
                                           type => 'Glib::String',
                                           attr => 'hidden' );

# Set up a SimpleList
$slist = Gtk2::Ex::Simple::List -> new('#' => 'int',
                                       $d->get('Thumbnails') => 'pixbuf',
                                       'Filename' => 'hstring',
                                       'Buffer' => 'hstring',
                                       'Resolution' => 'hstring');

# 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 FALSE;
});

# 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 FALSE;
});

# If dragged below the bottom of the window, scroll it.
$slist->signal_connect('drag-motion' => sub {
 my ($tree, $dnd, $x, $y, $t) = @_;
 my ($path, $how) = $tree->get_dest_row_at_pos($x, $y) or return;
 my $scroll = $tree->parent;
 $tree->set_drag_dest_row($path, $how);

 my $adj = $scroll->get_vadjustment;
 my ($value, $step) = ($adj->value, $adj->step_increment);

 if ($y > $adj->page_size - $step/2) {
  my $v = $value + $step;
  my $m = $adj->upper - $adj->page_size;
  $adj->set_value($v > $m ? $m : $v);
 }
 elsif ($y < ($tree->get_headers_visible ? $step : $step/2)) {
  my $v = $value - $step;
  my $m = $adj->lower;
  $adj->set_value($v < $m ? $m : $v);
 }

 return FALSE;
});

# 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($slist);

# VPaned for detail view and OCR buffer
my $vpaned = Gtk2::VPaned -> new;
$hpaned -> pack2 ($vpaned, TRUE, TRUE);

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

my $scale;
my $image = Gtk2::Image -> new;

# Need to pack the image in an eventbox to get it to respond to mouse clicks
my $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);

# OCR buffer
my $scwin_buffer = Gtk2::ScrolledWindow->new;
$scwin_buffer -> set_policy ('never', 'automatic');
$scwin_buffer -> set_shadow_type('etched-in');
my $textbuffer = Gtk2::TextBuffer->new;
my $textview = Gtk2::TextView->new_with_buffer($textbuffer);
$textview->set_wrap_mode ('word-char');
$textview -> set_sensitive(FALSE);
$scwin_buffer->add($textview);
$vpaned -> pack2 ($scwin_buffer, TRUE, TRUE);
if (defined($SETTING{'ocr panel'})) {
 $vpaned -> set_position ($SETTING{'ocr panel'});
}
else {
# $vpaned -> allocation -> height gives 1
#  my $height = $vpaned -> allocation -> height;
 my ($width, $height) = $window->get_size;
 $vpaned -> set_position ($height*3/4);
}

# Keep the simple list buffer up to date
$textbuffer -> {signalid} = $textbuffer -> signal_connect( changed => sub {
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 my @page = $slist -> get_selected_indices;
 $slist -> {data}[$page[0]][3] =
  $textbuffer->get_text ($textbuffer->get_start_iter, $textbuffer->get_end_iter, FALSE)
   if ($#page > -1);
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});
} );

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

# Display the new image
 if (@page) {

  my $path = Gtk2::TreePath->new_from_indices($page[0]);
  $slist->scroll_to_cell($path);

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

  $image -> set_from_pixbuf(
                    get_pixbuf($slist->{data}[$page[0]][2], $heightd, $widthd));
  $image -> show;

# Update the buffer, if created
  $textview -> set_sensitive(TRUE) if (! $textview -> is_sensitive);
  if (defined $slist -> {data}[$page[0]][3]) {
   $textbuffer->set_text ($slist -> {data}[$page[0]][3]);
  }
  else {
   $textbuffer -> delete($textbuffer->get_start_iter, $textbuffer->get_end_iter);
  }
 }
 else {
  $image -> clear;
  $textview -> set_sensitive(FALSE);
  $textbuffer -> set_text('');
 }
});

# _after ensures that Editables get first bite
$window -> signal_connect_after (key_press_event => sub {
 my ($widget, $event) = @_;

# Let the keypress propagate
 return FALSE unless ($event->keyval == $Gtk2::Gdk::Keysyms{Delete});

 delete_pages();
 return TRUE;
});

# 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'),
                                   }
                       },
	    'compression'     => { string => $d->get('Compression'),
				   values => {
					     'None' => $d->get('None'),
					     'JPEG' => $d->get('JPEG'),
					     },
				 },
            'resolution'      => { string => $d->get('Resolution') },
            'brightness'      => { string => $d->get('Brightness') },
            'contrast'        => { string => $d->get('Contrast') },
            'threshold'       => { string => $d->get('Threshold') },
            'speed'           => { string => $d->get('Speed'),
                                   values => {
                                             'yes' => $d->get('Yes'),
                                             'no'  => $d->get('No'),
                                             },
                                 },
            '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'),
                                             },
                                 },
            'calibration-cache' => { string => $d->get('Cache calibration'),
                                   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'),
                                   },
                                 },
          );

# Set up hash for unpaper options
my $unpaper_options = {
 layout => {
  type => 'ComboBox',
  string  => $d->get('Layout'),
  options => {
   single => {
    string => $d->get('Single'),
    tooltip => $d->get('One page per sheet, oriented upwards without rotation.'),
   },
   double => {
    string => $d->get('Double'),
    tooltip => $d->get('Two pages per sheet, landscape orientation (one page on the left half, one page on the right half).'),
   },
  },
  default => 'single',
 },
 'no-deskew' => {
  type => 'CheckButton',
  string => $d->get('No deskew'),
  tooltip => $d->get('Disable deskewing.'),
  default => FALSE,
 },
 'no-mask-scan' => {
  type => 'CheckButton',
  string => $d->get('No mask scan'),
  tooltip => $d->get('Disable mask detection.'),
  default => FALSE,
 },
 'no-blackfilter' => {
  type => 'CheckButton',
  string => $d->get('No black filter'),
  tooltip => $d->get('Disable black area scan.'),
  default => FALSE,
 },
 'no-grayfilter' => {
  type => 'CheckButton',
  string => $d->get('No gray filter'),
  tooltip => $d->get('Disable gray area scan.'),
  default => FALSE,
 },
 'no-noisefilter' => {
  type => 'CheckButton',
  string => $d->get('No noise filter'),
  tooltip => $d->get('Disable noise filter.'),
  default => FALSE,
 },
 'no-blurfilter' => {
  type => 'CheckButton',
  string => $d->get('No blur filter'),
  tooltip => $d->get('Disable blur filter.'),
  default => FALSE,
 },
 'no-border-scan' => {
  type => 'CheckButton',
  string => $d->get('No border scan'),
  tooltip => $d->get('Disable border scanning.'),
  default => FALSE,
 },
 'no-border-align' => {
  type => 'CheckButton',
  string => $d->get('No border align'),
  tooltip => $d->get('Disable aligning of the area detected by border scanning.'),
  default => FALSE,
 },
 'deskew-scan-direction' => {
  type => 'CheckButtonGroup',
  string => $d->get('Deskew to edge'),
  tooltip => $d->get("Edges from which to scan for rotation. Each edge of a mask can be used to detect the mask's rotation. If multiple edges are specified, the average value will be used, unless the statistical deviation exceeds --deskew-scan-deviation."),
  options => {
   left => {
    type => 'CheckButton',
    string => $d->get('Left'),
    tooltip => $d->get("Use 'left' for scanning from the left edge."),
   },
   top => {
    type => 'CheckButton',
    string => $d->get('Top'),
    tooltip => $d->get("Use 'top' for scanning from the top edge."),
   },
   right => {
    type => 'CheckButton',
    string => $d->get('Right'),
    tooltip => $d->get("Use 'right' for scanning from the right edge."),
   },
   bottom => {
    type => 'CheckButton',
    string => $d->get('Bottom'),
    tooltip => $d->get("Use 'bottom' for scanning from the bottom."),
   },
  },
  default => 'left,right',
 },
 'border-align' => {
  type => 'CheckButtonGroup',
  string => $d->get('Align to edge'),
  tooltip => $d->get('Edge to which to align the page.'),
  options => {
   left => {
    type => 'CheckButton',
    string => $d->get('Left'),
    tooltip => $d->get("Use 'left' to align to the left edge."),
   },
   top => {
    type => 'CheckButton',
    string => $d->get('Top'),
    tooltip => $d->get("Use 'top' to align to the top edge."),
   },
   right => {
    type => 'CheckButton',
    string => $d->get('Right'),
    tooltip => $d->get("Use 'right' to align to the right edge."),
   },
   bottom => {
    type => 'CheckButton',
    string => $d->get('Bottom'),
    tooltip => $d->get("Use 'bottom' to align to the bottom."),
   },
  },
 },
 'border-margin' => {
  type => 'SpinButtonGroup',
  string => $d->get('Border margin'),
  options => {
   vertical => {
    type => 'SpinButton',
    string => $d->get('Vertical margin'),
    tooltip => $d->get('Vertical distance to keep from the sheet edge when aligning a border area.'),
    min => 0,
    max => 10,
    step => 1,
   },
   horizontal => {
    type => 'SpinButton',
    string => $d->get('Horizontal margin'),
    tooltip => $d->get('Horizontal distance to keep from the sheet edge when aligning a border area.'),
    min => 0,
    max => 10,
    step => 1,
   },
  },
 },
 'white-threshold' => {
  type => 'SpinButton',
  string => $d->get('White threshold'),
  tooltip => $d->get('Brightness ratio above which a pixel is considered white.'),
  min => 0,
  max => 1,
  step => .01,
  default => 0.9,
 },
 'black-threshold' => {
  type => 'SpinButton',
  string => $d->get('Black threshold'),
  tooltip => $d->get('Brightness ratio below which a pixel is considered black (non-gray). This is used by the gray-filter. This value is also used when converting a grayscale image to black-and-white mode.'),
  min => 0,
  max => 1,
  step => .01,
  default => 0.33,
 },
};

update_uimanager();

$window -> show_all;
Gtk2 -> main;



### Subroutines

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

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

# Create a Gtk2::UIManager instance     
 $uimanager = Gtk2::UIManager->new;

# extract the accelgroup and add it to the window
 my $accelgroup = $uimanager->get_accel_group;
 $window->add_accel_group($accelgroup);

# Create the basic Gtk2::ActionGroup instance
# and fill it with Gtk2::Action instances
 my $actions_basic = Gtk2::ActionGroup->new ("actions_basic");
 $actions_basic->add_actions (\@action_items, undef);
	
# Add the actiongroup to the uimanager  
 $uimanager->insert_action_group($actions_basic, 0);

# Create the frontend Gtk2::ActionGroup instance
# and fill it with Gtk2::RadioAction instances
 my $actions_frontends = Gtk2::ActionGroup->new ("frontends");
 $actions_frontends->add_radio_actions(\@frontends, 0, \&change_frontend);

# Add the actiongroup to the uimanager          
 $uimanager->insert_action_group($actions_frontends, 0);

# Create the options Gtk2::ActionGroup instance
# and fill it with Gtk2::ToggleAction instances
 my $actions_options = Gtk2::ActionGroup->new ("options");
 $actions_options->add_toggle_actions ( [
  [ 'Options', 'gtk-preferences', $d->get('Enable Save _Options'), undef, $d->get('View options before saving'), undef, TRUE ],
  [ 'RestoreWindow', 'gtk-preferences', $d->get('Restore _window settings on startup'), undef, $d->get('Restore window settings on startup'), undef, TRUE ],
 ] );

# Add the actiongroup to the uimanager          
 $uimanager->insert_action_group($actions_options, 0);

# add the basic XML description of the GUI
 $uimanager->add_ui_from_string ($ui);

# extract the menubar
 my $menubar = $uimanager->get_widget('/MenuBar');

# Check for presence of various packages
 $dependencies{pdfapi2} = eval { require PDF::API2 };
 $dependencies{perlmagick} = eval { require Image::Magick };
 $dependencies{imagemagick} = check_command('convert');
 $dependencies{scanadf} = check_command('scanadf');
 $dependencies{xdg} = check_command('xdg-email');
 $dependencies{gocr} = check_command('gocr');
 $dependencies{tesseract} = check_command('tesseract');
 $dependencies{djvu} = check_command('cjb2');
 $dependencies{unpaper} = check_command('unpaper');
 $dependencies{libtiff} = check_command('tiffcp');
 if ($debug) {
  warn "Found PDF::API2\n" if ($dependencies{pdfapi2});
  warn "Found Image::Magick\n" if ($dependencies{perlmagick});
  warn "Found ImageMagick\n" if ($dependencies{imagemagick});
  warn "Found scanadf\n" if ($dependencies{scanadf});
  warn "Found xdg-email\n" if ($dependencies{xdg});
  warn "Found gocr\n" if ($dependencies{gocr});
  warn "Found tesseract\n" if ($dependencies{tesseract});
  warn "Found cjb2 (djvu)\n" if ($dependencies{djvu});
  warn "Found unpaper\n" if ($dependencies{unpaper});
  warn "Found libtiff\n" if ($dependencies{libtiff});
 }

# OCR engine options
 push @ocr_engine,
  [ 'gocr', $d->get('GOCR'), $d->get('Process image with GOCR.') ]
  if ($dependencies{gocr});
 push @ocr_engine,
  [ 'tesseract',  $d->get('Tesseract'),  $d->get('Process image with Tesseract.') ],
  if ($dependencies{tesseract});

 my $msg = '';
 $msg .= $d->get("PDF creation requires PDF::API2\n")
  if (! $dependencies{pdfapi2});

# Ghost scanadf item if scanadf or imagemagick not available
 my $item = $uimanager->get_widget('/MenuBar/Edit/Frontend/scanadf');
 if (! $dependencies{scanadf}) {
  $item -> set_sensitive(FALSE);
  $SETTING{'frontend'} = 'scanimage';
  $msg .= $d->get("The scanadf frontend is not available\n")
 }

# Set scanadf active if in config
 elsif (defined($SETTING{'frontend'}) and $SETTING{'frontend'} eq 'scanadf') {
  $item -> set_active(TRUE);
 }
# if scanadf isn't set, make sure that scanimage is
 else {
  $SETTING{'frontend'} = 'scanimage';
 }

# Disable options if necessary
 $uimanager->get_widget('/MenuBar/Edit/Options') -> set_active(FALSE)
  if (defined($SETTING{'enable options'}) and ! $SETTING{'enable options'});

# Disable restore window if necessary
 $uimanager->get_widget('/MenuBar/Edit/RestoreWindow') -> set_active(FALSE)
  if (defined($SETTING{'restore window'}) and ! $SETTING{'restore window'});

# Ghost save image item if imagemagick not available
 $msg .= $d->get("Save image and Save as PDF both require imagemagick\n")
  if (! $dependencies{imagemagick});

# Ghost save image item if libtiff not available
 $msg .= $d->get("Save image requires libtiff\n")
  if (! $dependencies{libtiff});

# Ghost djvu item if cjb2 not available
 $msg .= $d->get("Save as DjVu requires djvulibre-bin\n")
  if (! $dependencies{djvu});

# Ghost email item if xdg-email not available
 $msg .= $d->get("Email as PDF requires xdg-email\n")
  if (! $dependencies{xdg});

# Undo/redo start off ghosted anyway-
 $uimanager->get_widget('/MenuBar/Edit/Undo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Edit/Redo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Redo') -> set_sensitive(FALSE);

# save * start off ghosted anyway-
 $uimanager->get_widget('/MenuBar/File/Save PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Save image') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Save DjVu') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Email as PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Save PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Save image') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Email as PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Save PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Save image') -> set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Save DjVu') -> set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Email as PDF') -> set_sensitive(FALSE);

# a convenient place to put these
 $dependencies{pages} = -1;

# Ghost rotations and unpaper if perlmagick not available
 if (! $dependencies{perlmagick}) {
  $uimanager->get_widget('/MenuBar/View/Rotate 90') -> set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/View/Rotate 180') -> set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/View/Rotate 270') -> set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 90') -> set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 180') -> set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 270') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 90') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 180') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 270') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 90') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 180') -> set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 270') -> set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/Tools/unpaper') -> set_sensitive(FALSE);
  $msg .= $d->get("The rotating options, unpaper support and the scanadf frontend require perlmagick\n");
 }

# Ghost unpaper item if unpaper not available
 if (! $dependencies{unpaper}) {
  $uimanager->get_widget('/MenuBar/Tools/unpaper') -> set_sensitive(FALSE);
  $msg .= $d->get("unpaper missing\n");
 }

# Ghost ocr item if gocr and tesseract not available
 if (! $dependencies{gocr} and ! $dependencies{tesseract}) {
  $uimanager->get_widget('/MenuBar/Tools/OCR') -> set_sensitive(FALSE);
  $msg .= $d->get("OCR requires gocr or tesseract\n");
 }

# Put up warning if needed
 if ((! defined($SETTING{'startup warning'})
      or $SETTING{'startup warning'} eq TRUE) and ($msg ne '')) {
  my $dialog = Gtk2::Dialog -> new ($d->get('Warning: missing packages'),
                                    $window, 'destroy-with-parent',
                                    'gtk-ok' => 'none');
  my $label = Gtk2::Label->new ($msg);
  $dialog->vbox->add ($label);
  my $cb = Gtk2::CheckButton->new_with_label (
                                     $d->get("Don't show this message again"));
  $cb->set_active (TRUE);
  $dialog->vbox->add ($cb);
  $dialog->show_all;
  $dialog -> run;
  $SETTING{'startup warning'} = FALSE if ($cb->get_active);
  $dialog -> destroy;
 }

# extract the toolbar
 my $toolbar = $uimanager->get_widget('/ToolBar');

# turn off labels
 my $settings = $toolbar->get_settings();
 $settings->set('gtk-toolbar-style', 'icons'); # only icons

 return ( $menubar, $toolbar );
}


# ghost or unghost as necessary as # pages > 0 or not.

sub update_uimanager {
 my @widgets = ( '/MenuBar/View/Zoom 100',
                 '/MenuBar/View/Zoom to fit',
                 '/MenuBar/View/Zoom in',
                 '/MenuBar/View/Zoom out',
                 '/MenuBar/View/Rotate 90',
                 '/MenuBar/View/Rotate 180',
                 '/MenuBar/View/Rotate 270',
                 '/MenuBar/Tools/unpaper',
                 '/MenuBar/Tools/OCR',

                 '/ToolBar/Zoom 100',
                 '/ToolBar/Zoom to fit',
                 '/ToolBar/Zoom in',
                 '/ToolBar/Zoom out',
                 '/ToolBar/Rotate 90',
                 '/ToolBar/Rotate 180',
                 '/ToolBar/Rotate 270',

                 '/Detail_Popup/Zoom 100',
                 '/Detail_Popup/Zoom to fit',
                 '/Detail_Popup/Zoom in',
                 '/Detail_Popup/Zoom out',
                 '/Detail_Popup/Rotate 90',
                 '/Detail_Popup/Rotate 180',
                 '/Detail_Popup/Rotate 270',

                 '/Thumb_Popup/Rotate 90',
                 '/Thumb_Popup/Rotate 180',
                 '/Thumb_Popup/Rotate 270', );

 if ($slist -> get_selected_indices) {
  foreach (@widgets) {
   $uimanager->get_widget($_) -> set_sensitive(TRUE);
  }
 }
 else {
  foreach (@widgets) {
   $uimanager->get_widget($_) -> set_sensitive(FALSE);
  }
 }
 if ($#{$slist -> {data}} > -1) {
  if ($dependencies{pages} == -1) {
   if ($dependencies{djvu}) {
    $uimanager->get_widget('/MenuBar/File/Save DjVu') -> set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Save DjVu') -> set_sensitive(TRUE);
   }
   if ($dependencies{xdg}) {
    $uimanager->get_widget('/MenuBar/File/Email as PDF') -> set_sensitive(TRUE);
    $uimanager->get_widget('/ToolBar/Email as PDF') -> set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Email as PDF') -> set_sensitive(TRUE);
   }
   if ($dependencies{imagemagick}) {
    $uimanager->get_widget('/MenuBar/File/Save PDF') -> set_sensitive(TRUE);
    $uimanager->get_widget('/ToolBar/Save PDF') -> set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Save PDF') -> set_sensitive(TRUE);
    if ($dependencies{libtiff}) {
     $uimanager->get_widget('/MenuBar/File/Save image') -> set_sensitive(TRUE);
     $uimanager->get_widget('/ToolBar/Save image') -> set_sensitive(TRUE);
     $uimanager->get_widget('/Thumb_Popup/Save image') -> set_sensitive(TRUE);
    }
   }

   $dependencies{pages} = $#{$slist -> {data}};
  }
 }
 else {
  if ($dependencies{pages} > -1) {
   if ($dependencies{djvu}) {
    $uimanager->get_widget('/MenuBar/File/Save DjVu') -> set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Save DjVu') -> set_sensitive(FALSE);
    $windowv->hide if defined $windowv;
   }
   if ($dependencies{xdg}) {
    $uimanager->get_widget('/MenuBar/File/Email as PDF') -> set_sensitive(FALSE);
    $uimanager->get_widget('/ToolBar/Email as PDF') -> set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Email as PDF') -> set_sensitive(FALSE);
    $windowe->hide if defined $windowe;
   }
   if ($dependencies{imagemagick}) {
    $uimanager->get_widget('/MenuBar/File/Save PDF') -> set_sensitive(FALSE);
    $uimanager->get_widget('/ToolBar/Save PDF') -> set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Save PDF') -> set_sensitive(FALSE);
    if ($dependencies{libtiff}) {
     $uimanager->get_widget('/MenuBar/File/Save image') -> set_sensitive(FALSE);
     $uimanager->get_widget('/ToolBar/Save image') -> set_sensitive(FALSE);
     $uimanager->get_widget('/Thumb_Popup/Save image') -> set_sensitive(FALSE);
    }
   }
   $windowp->hide if defined $windowp;
   $windowi->hide if defined $windowi;

   $dependencies{pages} = $#{$slist -> {data}};
  }
 }
}


# Callback from RadioItem Edit/Frontend

sub change_frontend {
 my ($action, $current) = @_;
 $SETTING{'frontend'} = $current->get_name;
 rescan_options($vboxd, $device[$combobd -> get_active]) if (defined $windows);
}


# Zoom the detail window

sub zoom_button {
 my ($button) = @_;

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

 my @page = $slist -> get_selected_indices;
 my $filename = $slist->{data}[$page[0]][2];

 if ($button eq '100%') {
  $pixbuf = Gtk2::Gdk::Pixbuf -> new_from_file ($filename);
  my $widthi = $pixbuf->get_width;
  my $heighti =  $pixbuf->get_height;
  if ($widthd > $border) {
   $scale = $widthi/($widthd-$border) > $heighti/($heightd-$border)
                     ? $widthi/($widthd-$border) : $heighti/($heightd-$border);
  }
  else {
   return ($heightd-$border)/$heighti;
  }
 }
 elsif ($button eq 'in') {
  $scale *= 1.2;
  $pixbuf = Gtk2::Gdk::Pixbuf ->
     new_from_file_at_scale ($filename, $widthd*$scale, $heightd*$scale, TRUE);
 }
 elsif ($button eq 'out') {
  $scale /= 1.2;
  $pixbuf = Gtk2::Gdk::Pixbuf ->
     new_from_file_at_scale ($filename, $widthd*$scale, $heightd*$scale, TRUE);
 }
 else {
  $scale = 1;
  $pixbuf = Gtk2::Gdk::Pixbuf ->
                   new_from_file_at_scale ($filename, $widthd, $heightd, TRUE);
 }

 $image -> set_from_pixbuf($pixbuf);
 $image -> show;
}


# Returns the pixbuf scaled to fit in the given box

sub get_pixbuf {
 my ($filename, $height, $width) = @_;
 
 my $pixbuf;
 eval { $pixbuf = Gtk2::Gdk::Pixbuf ->
                  new_from_file_at_scale ($filename, $width, $height, TRUE); };
# if (Glib::Error::matches ($@, 'Mup::Thing::Error', 'flop')) {
#  recover_from_a_flop ();
# }
 if ($@) {
  warn $d->get('Warning: ')."$@\n";
  eval { $pixbuf = Gtk2::Gdk::Pixbuf ->
                  new_from_file_at_scale ($filename, $width, $height, TRUE); };
  if ($@) {
   return FALSE;
  }
  else {
   warn sprintf($d->get("Information: got %s on second attempt\n"), $filename);
  }
 }

 $scale = 1;
 return $pixbuf;
}


# Deletes all scans after warning.

sub new {

# Update undo/redo buffers
 take_snapshot();

# Deselect everything to prevent error removing selected thumbs
 $slist -> get_selection -> unselect_all;

# Depopulate the thumbnail list
 @{$slist -> {data}} = ();

 update_uimanager();
}


sub convert_to_tiff {
 my ($filename) = @_;
 my $image = Image::Magick->new;
 my $x = $image->Read($filename);
 warn "$x" if "$x";

# Calculate the resolution
 my $height = $image->Get('height');
 my $width = $image->Get('width');
 my $ratio = $height/$width;
 $ratio = 1/$ratio if ($ratio < 1);
 my $density = 72;
 for (my $i = 0; $i <= $#paperg; ++$i) {
  if ($xg[$i] > 0 and abs($ratio - $yg[$i]/$xg[$i]) < 0.01) {
   $density = (($height > $width) ? $height : $width)/$yg[$i]*25.4;
  }
 }

# Write the tif
 $image->Write(units => 'PixelsPerInch',
               density => $density,
               filename => "$filename.tif");
 return "$filename.tif";
}


# 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 image from file'),
                                      $window, 'open',
                                      'gtk-cancel' => 'cancel',
                                      'gtk-ok' => 'ok');
 $file_chooser -> set_select_multiple(TRUE);
 $file_chooser -> set_default_response('ok');

 if ('ok' eq $file_chooser->run) {

# cd back to tempdir to import
  chdir $dir;

# Update undo/redo buffers
  take_snapshot();

  my @filename = $file_chooser -> get_filenames;
  $file_chooser -> destroy;

  my $dialog = Gtk2::Dialog -> new ($d->get('Importing')."...", $windowo,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');

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

# Ensure that the dialog box is destroyed when the user responds.
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   kill_subs();
  });
  $dialog -> show_all;

# Install a handler for child processes
  $SIG{CHLD} = \&sig_child;

  my $pid = start_process(sub {

   my $j = 0;
   foreach my $filename (@filename) {

# Update cwd
    $SETTING{'cwd'} = dirname($filename);
    warn "Importing $filename\n" if ($debug);

# Get file type
    my $image = Image::Magick->new;
    my $x = $image->Read($filename);
    warn "$x" if "$x";

    my $format = $image->Get('format');
    warn "Format $format\n" if ($debug and defined($format));
    undef $image;

    if (! defined $format) {
     send($writer, sprintf("%f %s %i ", -1, 0, 0)
      .sprintf($d->get("%s is not a recognised image type"), $filename), 0);
    }
    elsif ($format eq 'Portable Document Format') {

# Extract images from PDF
     send($writer, sprintf("%f %s %i ", $j/($#filename+1), 0, 0)
      .$d->get('Extracting images from PDF'), 0);
     system ("pdfimages \"$filename\" x");

# Import each image
     my @images = <x-???.???>;
     my $i = 0;
     foreach (@images) {
      my ($filename, $resolution) = prepare_import($_, 'Portable anymap', undef, TRUE);
      send($writer, 
       sprintf("%f %s %i ", (++$i/($#images+1)+$j)/($#filename+1), $filename, $resolution)
       .sprintf($d->get("Importing image %i of %i"), $i, $#images+1), 0);
     }
    }
    elsif ($format eq 'Tagged Image File Format') {

# Split the tiff into its pages and import them individually
     send($writer, sprintf("%f %s %i ", $j/($#filename+1), 0, 0)
      .$d->get('Extracting images from TIFF'), 0);
     system("tiffsplit \"$filename\"");

     my @images = <x???.tif>;
     my $i = 0;
     foreach (@images) {
      my ($filename, $resolution) = prepare_import($_, $format, undef, TRUE);
      send($writer, 
       sprintf("%f %s %i ", (++$i/($#images+1)+$j)/($#filename+1), $filename, $resolution)
       .sprintf($d->get("Importing image %i of %i"), $i, $#images+1), 0);
     }
    }
    elsif ($format =~ /(Portable anymap)|(Portable Network Graphics)|(Joint Photographic Experts Group JFIF format)|(CompuServe graphics interchange format)/) {
     ($filename, my $resolution) = prepare_import($filename, $format);
     send($writer, sprintf("%f %s %i ", $j/($#filename+1), $filename, $resolution)
      .sprintf($d->get("Importing %s"), $format), 0);
    }
    else {
     my $tiff = convert_to_tiff($filename);
     $format = 'Tagged Image File Format';
     my ($filename, $resolution) = prepare_import($tiff, $format, undef, TRUE);
     send($writer, sprintf("%f %s %i ", $j/($#filename+1), $filename, $resolution)
      .sprintf($d->get("Importing %s"), $format), 0);
    }

    ++$j;
   }
   send($writer, 2, 0);
  });

  $helperTag{$pid} = Glib::IO->add_watch($reader->fileno(), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if (defined($line) and $line ne '') {
     my ($fraction, $filename, $resolution, @text) = split ' ', $line;
     if ($fraction == -1) {
      show_message_dialog($window, 'error', 'close', join(' ', @text));
     }
     elsif ($fraction > 1) {
      $dialog -> destroy;
      update_uimanager();
      return FALSE;  # uninstall
     }
     else {
      $pbar->set_fraction($fraction);
      $pbar->set_text(join ' ', @text);
      Gtk2->main_iteration while Gtk2->events_pending;
      add_image($filename, undef, $resolution) if ($filename ne '0');
     }
    }
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    $dialog -> destroy;
    update_uimanager();
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  $file_chooser -> destroy;
 }

# cd back to tempdir
 chdir $dir;
}


# Create $window_new transient to $window with $title and $vbox

sub create_window {
 my ($window, $title, $destroy) = @_;
 my $window_new = Gtk2::Window -> new;
 $window_new -> set_border_width($border_width);
 $window_new -> set_title ($title);
 if ($destroy) {
  $window_new -> signal_connect (destroy => sub { $window_new -> destroy; } );
 }
 else {
  $window_new -> signal_connect (delete_event => sub {
   $window_new -> hide;
   return TRUE; # ensures that the window is not destroyed
  });
 }
 $window_new -> set_transient_for($window); # Assigns parent
 $window_new->set_position ('center-on-parent');

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

 return ($window_new, $vbox);
}


# Add a frame and radio buttons to $vbox,
# returning $buttona, $buttonc, $buttons

sub add_page_range {
 my ($vbox) = @_;

# Frame for page range
 my $frame = Gtk2::Frame -> new($d->get('Page range'));
 $vbox -> pack_start ($frame, TRUE, TRUE, 0);
 my $vboxp = Gtk2::VBox -> new;
 $vboxp -> set_border_width($border_width);
 $frame -> 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'));
 $buttona -> signal_connect ('toggled' => sub {
  $SETTING{'Page range'} = 'all' if ($buttona -> get_active);
 });
 $vboxp -> pack_start($buttona, TRUE, TRUE, 0);
 my @group = $buttona -> get_group;

# Current button
 my $buttonc = Gtk2::RadioButton -> new(@group, $d->get('Current'));
 $buttonc -> signal_connect ('toggled' => sub {
  $SETTING{'Page range'} = 'current' if ($buttonc -> get_active);
 });
 $vboxp -> pack_start($buttonc, TRUE, TRUE, 0);

# Selected button
 my $buttons = Gtk2::RadioButton -> new(@group, $d->get('Selected'));
 $buttons -> signal_connect ('toggled' => sub {
  $SETTING{'Page range'} = 'selected' if ($buttons -> get_active);
 });
 $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);
 }
 
 return ($buttona, $buttonc, $buttons);
}


# return string of filenames depending on which radiobutton is active

sub get_pagelist {
 my $n;
 my $pagelist;
 if ($SETTING{'Page range'} eq 'all') {
  $n = $#{$slist -> {data}};
  $pagelist = $slist -> {data}[0][2];
  my $i = 1;
  while ($i <= $#{$slist -> {data}}) {
   $pagelist = $pagelist." ".$slist -> {data}[$i][2];
   ++$i;
  }
 }
 elsif ($SETTING{'Page range'} eq 'current') {
  $n = 1;
  my @page = $slist -> get_selected_indices;
  $pagelist = $slist -> {data}[$page[0]][2];
 }
 elsif ($SETTING{'Page range'} eq '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;
  }
 }
 
 return ($pagelist, $n);
}


# return array index of filenames depending on which radiobutton is active

sub get_page_index {
 my @page;
 if ($SETTING{'Page range'} eq 'all') {
  my $i = 0;
  while ($i <= $#{$slist -> {data}}) {
   push @page, $i;
   ++$i;
  }
 }
 elsif ($SETTING{'Page range'} eq 'current') {
  push @page, ($slist -> get_selected_indices)[0];
 }
 elsif ($SETTING{'Page range'} eq 'selected') {
  @page = $slist -> get_selected_indices;
 }
 
 return @page;
}


# Add PDF options to $vbox

sub add_PDF_options {
 my ($vbox) = @_;

# Frame for metadata
 my $frame = Gtk2::Frame -> new($d->get('Metadata'));
 $vbox -> pack_start ($frame, TRUE, TRUE, 0);
 my $vboxm = Gtk2::VBox -> new;
 $vboxm -> set_border_width($border_width);
 $frame -> 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);
 $SETTING{'date offset'} = 0 if (! (defined($SETTING{'date offset'})));
 my ($day, $month, $year) =
                    (localtime(time+$SETTING{'date offset'}*24*60*60))[3, 4, 5];
 $year += 1900;
 $month += 1;

 my $button = Gtk2::Button -> new(sprintf("%04d-%02d-%02d", $year, $month, $day));
 $button -> signal_connect( clicked => sub {
  my ($window, $vbox) = create_window($windowp, $d->get('Select Date'), TRUE);
  $window->set_resizable(FALSE);

  my $calendar = Gtk2::Calendar -> new;
  $calendar -> select_day($day);
  $calendar -> select_month($month-1, $year);
  $calendar -> signal_connect(day_selected_double_click => sub {
   ($year, $month, $day) = $calendar -> get_date;
   $month += 1;
   $button -> set_label (sprintf("%04d-%02d-%02d", $year, $month, $day));
   use Time::Local;
   $SETTING{'date offset'} =
               int((timelocal(0, 0, 0, $day, $month-1, $year) - time)/60/60/24);
   $window -> destroy;
  });
  $vbox -> pack_start ($calendar, TRUE, TRUE, 0);

  my $today = Gtk2::Button -> new($d->get('Today'));
  $today -> signal_connect( clicked => sub {
   my ($day, $month, $year) = (localtime())[3, 4, 5];
   $year += 1900;
   $calendar -> select_day($day);
   $calendar -> select_month($month, $year);
  });
  $vbox -> pack_start ($today, TRUE, TRUE, 0);

  $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'}));

 return ($entrya, $entryt, $entrys, $entryk);
}


sub update_PDF_settings {
 my ($entrya, $entryt, $entrys, $entryk) = @_;

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


sub get_PDF_options {
 my %h;
 $h{'Author'} = $SETTING{'author'} if defined $SETTING{'author'};
 if (defined $SETTING{'date offset'}) {
  my ($day, $month, $year) =
                    (localtime(time+$SETTING{'date offset'}*24*60*60))[3, 4, 5];
  $year += 1900;
  $month += 1;
  $h{'CreationDate'} = sprintf ("D:%4i%02i%02i000000+00'00'",
                                                           $year, $month, $day);
  $h{'ModDate'} = sprintf ("D:%4i%02i%02i000000+00'00'", $year, $month, $day);
 }
 $h{'Creator'} = "$program v$version";
 $h{'Producer'} = "PDF::API2";
 $h{'Title'} = $SETTING{'title'} if defined $SETTING{'title'};
 $h{'Subject'} = $SETTING{'subject'} if defined $SETTING{'subject'};
 $h{'Keywords'} = $SETTING{'keywords'} if defined $SETTING{'keywords'};
 return %h;
}


# Create the PDF

sub create_PDF {
 my ($filename) = @_;

 require PDF::API2;

 my $dialog = Gtk2::Dialog -> new ($d->get('Saving PDF')."...", $windowo,
                                   'destroy-with-parent',
                                   'gtk-cancel' => 'cancel');

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

# Ensure that the dialog box is destroyed when the user responds.
 $dialog -> signal_connect (response => sub {
  $_[0] -> destroy;
  kill_subs();
 });
 $dialog -> show_all;

# Install a handler for child processes
 $SIG{CHLD} = \&sig_child;

 my $pid = start_process(sub {

# fill $pagelist with filenames depending on which radiobutton is active
  my @pagelist = get_page_index();
  my $page = 0;

# Create PDF with PDF::API2
  send($writer, '0'.$d->get('Setting up PDF'), 0);
  my $pdf = PDF::API2->new(-file => $filename);
  $pdf->info(get_PDF_options());

  foreach (@pagelist) {
   ++$page;
   send($writer, $page/($#pagelist+2)
    .sprintf($d->get("Saving page %i of %i"), $page, $#pagelist+1), 0);

   my $page = $pdf->page;

   my $filename = $slist -> {data}[$_][2];
   my $image = Image::Magick->new;
   my $x = $image->Read($filename);
   warn "$x" if "$x";

# Get the size and resolution
   my $w = $image->Get('width');
   my $h = $image->Get('height');
   my $resolution = $slist -> {data}[$_][4];

# Convert file if necessary
   my $format = $1 if ($filename =~ /\.(\w*)$/);
   if (defined($SETTING{'pdf compression'})
        and ($SETTING{'pdf compression'} ne 'none'
        and $SETTING{'pdf compression'} ne $format)
        or $SETTING{'downsample'}
        or $SETTING{'pdf compression'} eq 'jpg') {
    print "Converting $filename to " if ($debug);
    (undef, $filename) = tempfile(DIR => $dir,
                                  SUFFIX => '.'.$SETTING{'pdf compression'});
    print "$filename\n" if ($debug);

    my $depth = $image->Get('depth');
    if ($SETTING{'downsample'}) {
     $w = $w/$resolution*$SETTING{'downsample dpi'};
     $h = $h/$resolution*$SETTING{'downsample dpi'};
     $resolution = $SETTING{'downsample dpi'};
     $x = $image->Resize(width => $w, height => $h);
     warn "$x" if "$x";
    }
    $x = $image->Set(quality => $SETTING{quality})
     if $SETTING{'pdf compression'} eq 'jpg';
    warn "$x" if "$x";
    if ($depth > 1) {
     print "Maintaining image depth $depth\n" if $debug;
     $x = $image->Write(filename => $filename, depth => $depth);
    }
    else {
     $x = $image->Write(filename => $filename);
    }
    warn "$x" if "$x";
    $format = $1 if ($filename =~ /\.(\w*)$/);
   }

   print "Defining page at ", $w*72/$resolution, " x ", $h*72/$resolution, "\n"
    if $debug;
   $page->mediabox($w*72/$resolution, $h*72/$resolution);

# Add OCR as annotation
   if (defined($slist -> {data}[$_][3]) and $slist -> {data}[$_][3] ne '') {
    my $ant=$page->annotation;
    $ant->text($slist -> {data}[$_][3],
     -rect=>[0,0,$w*72/$resolution,$h*72/$resolution]);

# Add OCR as text behind the scan
    my $font = $pdf->corefont('Times-Roman');
    my $text = $page->text;
    my $size = 1;
    $text->font($font, $size);
    $text->strokecolor('white');
    $text->fillcolor('white');
    my $y = $h*72/$resolution;
    foreach my $line (split("\n", $slist -> {data}[$_][3])) {
     my $x = 0;

# Add a word at a time in order to linewrap
     foreach my $word (split(' ', $line)) {
      if (length($word)*$size+$x > $w) {
       $x = 0;
       $y -= $size;
      }
      $text -> translate($x, $y);
      $word = ' '.$word if ($x > 0);
      $x += $text->text($word);
     }
     $y -= $size;
    }
   }

# Add scan
   my $gfx = $page->gfx;
   my $imgobj;
   if ($format eq 'png') {
    eval {$imgobj = $pdf->image_png($filename)};
   }
   elsif ($format eq 'jpg') {
    eval {$imgobj = $pdf->image_jpeg($filename)};
   }
   elsif ($format eq 'pnm') {
    eval {$imgobj = $pdf->image_pnm($filename)};
   }
   elsif ($format eq 'gif') {
    eval {$imgobj = $pdf->image_gif($filename)};
   }
   elsif ($format eq 'tif') {
    eval {$imgobj = $pdf->image_tiff($filename)};
   }
   else {
    $@ = "Error embedding file $filename in $format format to PDF\n";
   }
   if ($@) {
    warn $@;
   }
   else {
    eval {$gfx->image($imgobj,0,0,72/$resolution)};
    if ($@) {
     warn $@;
    }
    else {
     print "Adding $format at $resolution dpi\n";
    }
   }
  }
  send($writer, '1'.$d->get('Closing PDF'), 0);
  $pdf->save;
  $pdf->end();
  send($writer, '2', 0);
 });

 $helperTag{$pid} = Glib::IO->add_watch($reader->fileno(), ['in', 'hup'], sub {
  my ($fileno, $condition) = @_;

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($reader, $line, 1000, 0);
   if ($line =~ /(\d*\.?\d*)(.*)/) {
    my $fraction=$1;
    my $text=$2;
    if ($fraction > 1) {
     $dialog -> destroy;
     return FALSE;  # uninstall
    }
    $pbar->set_fraction($fraction);
    $pbar->set_text($text);
   }
  }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
  if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   update_uimanager();
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
}


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

sub save_PDF {

# 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');
 $file_chooser -> set_default_response('ok');

 if ('ok' eq $file_chooser->run) {
  my $filename = $file_chooser -> get_filename;
  $filename = $filename.".pdf" if ($filename !~ /\.pdf$/i);
  if (file_exists($file_chooser, $filename)) {
   $file_chooser -> destroy;
   return TRUE;
  }

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

# Create the PDF
  create_PDF($filename);
  
  $windowp -> hide if defined $windowp;
 }

 $file_chooser -> destroy;

# cd back to tempdir
 chdir $dir;
}


# Set up quality spinbutton here so that it can be shown or hidden by callback

sub add_quality_spinbutton {

 my ($vbox) = @_;
 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('JPEG Quality'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);
 my $spinbutton = Gtk2::SpinButton -> new_with_range(1, 100, 1);
 if (defined($SETTING{'quality'})) {
  $spinbutton->set_value($SETTING{'quality'});
 }
 else {
  $spinbutton->set_value(75);
 }
 $hbox -> pack_end ($spinbutton, FALSE, FALSE, 0);
 return ($hbox, $spinbutton);
}


sub add_pdf_compression {
 my ($vbox) = @_;

# Downsample options
 my $hboxd = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxd, FALSE, FALSE, 0);
 my $button = Gtk2::CheckButton -> new($d->get('Downsample to'));
 $button->set_active(TRUE);
 $hboxd -> pack_start ($button, FALSE, FALSE, 0);
 my $spinbutton = Gtk2::SpinButton -> new_with_range(9, 2400, 1);
 $spinbutton->set_value($SETTING{'downsample dpi'});
 my $label = Gtk2::Label -> new ($d->get('dpi'));
 $hboxd -> pack_end ($label, FALSE, FALSE, 0);
 $hboxd -> pack_end ($spinbutton, FALSE, FALSE, 0);
 $button -> signal_connect (toggled => sub {
  if ($button->get_active) {
   $spinbutton->set_sensitive(TRUE);
  }
  else {
   $spinbutton->set_sensitive(FALSE);
  }
 });
 $button->set_active($SETTING{'downsample'});

# Compression options
 my @compression = (
  [ 'jpg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  [ 'png',  $d->get('PNG'),  $d->get('Compress output with PNG encoding.') ],
  [ 'none', $d->get('None'), $d->get('Use no compression algorithm on output.') ],
 );

# Compression ComboBox
 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Compression'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);

# Set up quality spinbutton here so that it can be shown or hidden by callback
 my ($hboxq, $spinbuttonq) = add_quality_spinbutton($vbox);
 my $combob =  combobox_from_array($SETTING{'pdf compression'}, @compression);
 $combob -> signal_connect (changed => sub {
  if ($compression[$combob->get_active][0] eq 'jpg') {
   $hboxq -> show_all;
  }
  else {
   $hboxq -> hide_all;
  }
 });
 $hbox -> pack_end ($combob, FALSE, FALSE, 0);

 return ($button, $spinbutton, $combob, $hboxq, $spinbuttonq, @compression);
}


sub save_PDF_dialog {

# $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
#warn "rmb pdf $SETTING{'RMB'} $SETTING{'Page range'}\n";

 if ($uimanager->get_widget('/MenuBar/Edit/Options') -> get_active) {

  if (defined $windowp) {
   $windowp -> present;
   return;
  }

# PDF pop-up window
  ($windowp, my $vbox) = create_window($window, $d->get('Save as PDF'), FALSE);

# PDF options
  my ($entrya, $entryt, $entrys, $entryk) = add_PDF_options ($vbox);

# Frame for page range
  my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

# Compression options
  my ($buttond, $spinbuttond, $combob, $hboxq, $spinbuttonq, @compression) = add_pdf_compression($vbox);

# HBox for buttons
  my $hboxb = Gtk2::HBox -> new;
  $vbox -> pack_start ($hboxb, FALSE, 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 {

# dig out the compression
   $SETTING{'downsample'} = $buttond->get_active;
   $SETTING{'downsample dpi'} = $spinbuttond->get_value;
   $SETTING{'pdf compression'} = $compression[$combob->get_active][0];
   $SETTING{'quality'} = $spinbuttonq->get_value;

   update_PDF_settings($entrya, $entryt, $entrys, $entryk);
   save_PDF();
  } );

# 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 ($compression[$combob->get_active][0] ne 'jpg');
 }
 else {
  save_PDF();
 }
}


# Display page selector and on save a fileselector.

sub save_image_dialog {

# $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 if (defined $windowi) {
  $windowi -> present;
  return;
 }

 ($windowi, my $vbox) = create_window($window, $d->get('Save image'), FALSE);

# Frame for page range
 my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

# Image type ComboBox
 my $hboxi = Gtk2::HBox -> new;
 $vbox -> pack_start($hboxi, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Image type'));
 $hboxi -> pack_start ($label, FALSE, FALSE, 0);

 my @type = (
  [ 'tif', $d->get('TIFF'), $d->get('Tagged Image File Format') ],
  [ 'png', $d->get('PNG'), $d->get('Portable Network Graphics') ],
  [ 'jpg', $d->get('JPEG'), $d->get('Joint Photographic Experts Group JFIF format') ],
  [ 'pnm', $d->get('PNM'), $d->get('Portable anymap') ],
  [ 'gif', $d->get('GIF'), $d->get('CompuServe graphics interchange format') ],
 );

 my @compression = (
  [ 'lzw', $d->get('LZW'), $d->get('Compress output with Lempel-Ziv & Welch encoding.') ],
  [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate encoding.') ],
  [ 'jpeg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  [ 'packbits', $d->get('Packbits'), $d->get('Compress output with Packbits encoding.') ],
  [ 'g3', $d->get('G3'), $d->get('Compress output with CCITT Group 3 encoding.') ],
  [ 'g4', $d->get('G4'), $d->get('Compress output with CCITT Group 4 encoding.') ],
  [ 'none', $d->get('None'), $d->get('Use no compression algorithm on output.') ],
 );

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

# Set up quality spinbutton here so that it can be shown or hidden by callback
 my ($hboxq, $spinbuttonq) = add_quality_spinbutton($vbox);

# Fill compression ComboBox
 my $combobc = combobox_from_array($SETTING{'tiff compression'}, @compression);
 $combobc -> signal_connect (changed => sub {
  if ($compression[$combobc->get_active][0] eq 'jpeg') {
   $hboxq -> show_all;
  }
  else {
   $hboxq -> hide_all;
  }
 });
 $hboxc -> pack_end ($combobc, FALSE, FALSE, 0);

# Fill image type ComboBox
 my $combobi = combobox_from_array($SETTING{'image type'}, @type);
 $combobi -> signal_connect (changed => sub {
  if ($type[$combobi->get_active][0] eq 'tif') {
   $hboxc -> show_all;
  }
  else {
   $hboxc -> hide_all;
  }
  if ($type[$combobi->get_active][0] eq 'jpg'
      or ($type[$combobi->get_active][0] eq 'tif'
          and $compression[$combobc->get_active][0] eq 'jpeg')) {
   $hboxq -> show_all;
  }
  else {
   $hboxq -> hide_all;
  }
 });
 $hboxi -> pack_end ($combobi, FALSE, FALSE, 0);

# HBox for buttons
 my $hboxb = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxb, FALSE, 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 {

# dig out the image type, compression and quality
  $SETTING{'image type'} = $type[$combobi->get_active][0];
  $SETTING{'tiff compression'} = $compression[$combobc->get_active][0];
  $SETTING{'quality'} = $spinbuttonq->get_value;

  if ($SETTING{'image type'} eq 'tif') {
   save_TIFF();
  }
  else {
   save_image();
  }
 } );

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

 $windowi -> show_all;
 $hboxq -> hide_all if ($type[$combobi->get_active][0] ne 'jpg'
                        or ($type[$combobi->get_active][0] eq 'tif'
                          and $compression[$combobc->get_active][0] ne 'jpeg'));
 $hboxc -> hide_all if ($type[$combobi->get_active][0] ne 'tif');
}


sub show_message_dialog {
 my ($parent, $type, $buttons, $text) = @_;
 my $dialog = Gtk2::MessageDialog ->
  new ($parent, 'destroy-with-parent', $type, $buttons, $text);
 my $response = $dialog -> run;
 $dialog -> destroy;
 return $response;
}


sub file_exists {
 my ($file_chooser, $filename) = @_;
 if (-e $filename) {
  my $response = show_message_dialog($file_chooser, 'question', 'ok-cancel',
             sprintf($d->get("File %s exists.\nReally overwrite?"), $filename));
  return TRUE if ($response ne 'ok');
 }
 return FALSE;
}


sub save_image {

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

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

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

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

# cd back to tempdir
  chdir $dir;

# fill $pagelist with filenames depending on which radiobutton is active
  my ($pagelist, $n) = get_pagelist();

  my @pagelist = split / /, $pagelist;
  if ($#pagelist == 0) {
   $filename = $filename.".$SETTING{'image type'}"
    if ($filename !~ /\.$SETTING{'image type'}$/i);
   system ("convert $pagelist[0] $filename")
    if (! file_exists($file_chooser, $filename));
  }
  else {
   my $i = 1;
   foreach (@pagelist) {
    system ("convert $_ \"$filename.$i.$SETTING{'image type'}\"")
     if (! file_exists($file_chooser, "$filename.$i.$SETTING{'image type'}"));
    $i++;
   }
  }
 }

 $file_chooser -> destroy;
}


sub save_TIFF {

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

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

 if ('ok' eq $file_chooser->run) {
  my $filename = $file_chooser -> get_filename;
  $filename = $filename.".tif" if ($filename !~ /\.tif$/i);
  if (file_exists($file_chooser, $filename)) {
   $file_chooser -> destroy;
   return TRUE;
  }

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

# cd back to tempdir
  chdir $dir;

  $file_chooser -> destroy;

  my $dialog = Gtk2::Dialog -> new ($d->get('Importing')."...", $windowo,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');

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

# Ensure that the dialog box is destroyed when the user responds.
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   kill_subs();
  });
  $dialog -> show_all;

# Install a handler for child processes
  $SIG{CHLD} = \&sig_child;

  my $pid = start_process(sub {

# fill $pagelist with filenames depending on which radiobutton is active
   my ($pagelist, $n) = get_pagelist();
   my $page = 0;

   my @pagelist = split / /, $pagelist;
   foreach (@pagelist) {
    ++$page;
    send($writer, ($page-1)/($#pagelist+2)
     .sprintf($d->get("Converting image %i of %i to TIFF"), $page, $#pagelist+1), 0);

    if ($_ !~ /\.tif/) {
     my (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');

# Convert to tiff
     system ("convert $_ $tif");

     $_ = $tif;
    }
   }
   $pagelist = "@pagelist";

   my $compression = $SETTING{'tiff compression'};
   $compression .= ':'.$SETTING{'quality'} if ($compression eq 'jpeg');

# Create the tiff
   send($writer, '1'.$d->get('Concatenating TIFFs'), 0);
   my $cmd = "tiffcp -c $compression $pagelist \"$filename\" 2>&1";
   print "$cmd\n" if $debug;
   my $output = `$cmd`;
   send($writer, "2$output", 0);
  });

  $helperTag{$pid} = Glib::IO->add_watch($reader->fileno(), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if ($line =~ /(\d*\.?\d*)(.*)/) {
     my $fraction=$1;
     my $text=$2;
     if ($fraction > 1) {
      $dialog -> destroy;
      if ($text eq '') {
       $windowi -> hide if defined $windowi;
      }
      else {
       unlink $filename;
       show_message_dialog($windowi, 'error', 'close', $text);
      }
      return FALSE;  # uninstall
     }
     $pbar->set_fraction($fraction);
     $pbar->set_text($text);
    }
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    $dialog -> destroy;
    update_uimanager();
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  $file_chooser -> destroy;
 }
}


# Display page selector and on save a fileselector.

sub save_djvu_dialog {

# $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 if ($uimanager->get_widget('/MenuBar/Edit/Options') -> get_active) {

  if (defined $windowv) {
   $windowv -> present;
   return;
  }

  ($windowv, my $vbox) = create_window($window, $d->get('Save as DjVu'), FALSE);

# Compression ComboBox
  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 $combob = Gtk2::ComboBox->new_text;

# Fill compression ComboBox
  $combob->append_text ($d->get('Bitonal'));
  $combob->set_active(0);
  $hboxc -> pack_end ($combob, FALSE, FALSE, 0);

# Frame for page range
  my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

# HBox for buttons
  my $hboxb = Gtk2::HBox -> new;
  $vbox -> pack_start ($hboxb, FALSE, 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 { save_djvu(); } );

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

  $windowv -> show_all;
 }
 else {
  save_djvu();
 }
}


sub save_djvu {

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

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

 if ('ok' eq $file_chooser->run) {
  my $filename = $file_chooser -> get_filename;
  $filename = $filename.".djvu" if ($filename !~ /\.djvu$/i);
  if (file_exists($file_chooser, $filename)) {
   $file_chooser -> destroy;
   return TRUE;
  }

  $windowv -> hide;

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

# cd back to tempdir
  chdir $dir;

  $file_chooser -> destroy;

  my $dialog = Gtk2::Dialog -> new ($d->get('Saving DjVu')."...", $windowo,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');

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

# Ensure that the dialog box is destroyed when the user responds.
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   kill_subs();
  });
  $dialog -> show_all;

# Install a handler for child processes
  $SIG{CHLD} = \&sig_child;

  my $pid = start_process(sub {

# fill $pagelist with filenames depending on which radiobutton is active
   my ($pagelist, $n) = get_pagelist();
   
   if ($n > 1) {
    my @pagelist = split / /, $pagelist;
    my $page = 0;
    foreach (@pagelist) {
     send($writer, $page/(($#pagelist+1)*2+1)
      .sprintf($d->get("Embedding image %i of %i in DjVu"),
                                                     $page+1, $#pagelist+1), 0);
     my (undef, $djvu) = tempfile(DIR => $dir, SUFFIX => '.djvu');

# Create the djvu
     my $cmd = "cjb2 $_ $djvu";
     print "$cmd\n" if $debug;
     system ($cmd);
     $_ = $djvu;
     ++$page;
    }
    send($writer, $page/(($#pagelist+1)*2+1).$d->get("Concatenating pages"), 0);
    my $cmd = "djvm -c '$filename' @pagelist";
    print "$cmd\n" if $debug;
    system ($cmd);
   }
   else {
    send($writer, '0'.sprintf($d->get("Embedding image %i of %i in DjVu"), 1, 1), 0);
    my $cmd = "cjb2 $pagelist '$filename'";
    print "$cmd\n" if $debug;
    system ($cmd);
   }
  
# Add OCR to text layer
   my @pagelist = get_page_index();
   for (my $i = 0; $i <= $#pagelist; $i++) {
    my $j = $pagelist[$i];
    if (defined($slist -> {data}[$j][3]) and $slist -> {data}[$j][3] ne '') {

     send($writer, ($#pagelist+$i)/(($#pagelist+1)*2+1)
      .sprintf($d->get("Embedding OCR output %i of %i in DjVu"),
                                                        $i+1, $#pagelist+1), 0);

     my $image = Image::Magick->new;
     my $x = $image->Read($slist -> {data}[$j][2]);
     warn "$x" if "$x";

# Get the size and resolution
     my $w = $image->Get('width');
     my $h = $image->Get('height');
     my $resolution = $slist -> {data}[$j][4];

# Escape any inverted commas
     my $txt = $slist -> {data}[$j][3];
     $txt =~ s/"/\\\"/g;

# Write djvusedtxtfile
     my $djvusedtxtfile = "djvusedtxtfile";
     open (FILE, "> $djvusedtxtfile")
      or die sprintf($d->get("Can't open file: %s"), $djvusedtxtfile);
     print FILE "(page 0 0 ", int($w*72/$resolution), " ", int($h*72/$resolution), "\n";
     print FILE "(line 0 0 ", int($w*72/$resolution), " ", int($h*72/$resolution), " \"";
     print FILE $txt, "\"))";
     close FILE;

# Write djvusedtxtfile
     my $cmd = sprintf "djvused '%s' -e 'select %i; set-txt djvusedtxtfile' -s", $filename, $i+1;
     print "$cmd\n" if $debug;
     system ($cmd);
    }
   }
   send($writer, "2", 0);
  });

  $helperTag{$pid} = Glib::IO->add_watch($reader->fileno(), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if ($line =~ /(\d*\.?\d*)(.*)/) {
     my $fraction=$1;
     my $text=$2;
     if ($fraction > 1) {
      $dialog -> destroy;
      if ($text eq '') {
       $windowi -> hide if defined $windowi;
      }
      else {
       unlink $filename;
       show_message_dialog($windowi, 'error', 'close', $text);
      }
      return FALSE;  # uninstall
     }
     $pbar->set_fraction($fraction);
     $pbar->set_text($text);
    }
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    $dialog -> destroy;
    update_uimanager();
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  $file_chooser -> destroy;
 }
}


# Display page selector and email.

sub email {

 if (defined $windowe) {
  $windowe -> present;
  return;
 }

# $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
 ($windowe, my $vbox) = create_window($window, $d->get('Email as PDF'), FALSE);

# PDF options
 my ($entrya, $entryt, $entrys, $entryk) = add_PDF_options ($vbox);

# Frame for page range
 my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

# Compression options
 my ($buttond, $spinbuttond, $combob, $hboxq, $spinbuttonq, @compression) = add_pdf_compression($vbox);

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

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

# Set options
  update_PDF_settings($entrya, $entryt, $entrys, $entryk);

# dig out the compression
  $SETTING{'downsample'} = $buttond->get_active;
  $SETTING{'downsample dpi'} = $spinbuttond->get_value;
  $SETTING{'pdf compression'} = $compression[$combob->get_active][0];
  $SETTING{'quality'} = $spinbuttonq->get_value;

  my (undef, $pdf) = tempfile(DIR => $dir, SUFFIX => '.pdf');

# Create the PDF
  create_PDF($pdf);

  system ("xdg-email --attach $pdf 'x\@y'");

  $windowe -> hide;

 } );

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

 $windowe -> show_all;
 $hboxq -> hide_all if ($compression[$combob->get_active][0] ne 'jpg');
}


# Scan

sub scan_dialog {

 if (defined $windows) {
  $windows -> present;
  return;
 }

# scan pop-up window
 ($windows, my $vbox) = create_window($window, $d->get('Scan Document'), FALSE);

 my $output;
 if (! @test) {

# Set up ProgressBar
  my $pbar = Gtk2::ProgressBar->new;
  $pbar -> set_pulse_step(.1);
  $pbar->set_text ($d->get('Fetching list of devices'));
  $vbox->pack_start ($pbar, FALSE, FALSE, 0);
  $windows -> show_all;
  my $running = TRUE;

# Timer will run until callback returns false 
  my $timer = Glib::Timeout->add (100, sub { if ($running) {
                                              $pbar->pulse;
                                              return TRUE;
                                             }
                                             else {
                                              return FALSE;
                                             } });

  my $cmd = "scanimage --formatted-device-list=\"'%i','%d','%v %m'\n\" 2>/dev/null";
  print "$cmd\n" if ($debug);

# Interface to frontend
  my $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
  warn "Forked PID $pid\n" if ($debug);
 
# Read without blocking
  Glib::IO->add_watch ( fileno($read), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    sysread $read, $line, 1024;
    $output .= $line;
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    close $read;
    warn 'Waiting to reap process' if ($debug);
    my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
    warn "Reaped PID $pid\n" if ($debug);
    $running = FALSE;
    $pbar -> destroy;

    if (! defined($output) or $output eq '') {
     $windows->destroy;
     undef $windows;
     show_message_dialog($window, 'error', 'close', $d->get('No scanners found'));
     return FALSE;
    }

    populate_scan_dialog($vbox, $output);
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  populate_scan_dialog($vbox, $output);
 }
}


sub populate_scan_dialog {
 my ($vbox, $output) = @_;

 print $output if ($debug and defined($output));

# 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);

# Need to define this here to reference it later.
 $vboxd = Gtk2::VBox -> new;
 my $ev = Gtk2::EventBox->new;
 $combobd = Gtk2::ComboBox->new_text;
 $ev->add($combobd);

# parse out the device and model names
 if (! @test) {
  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
   ($model[$1] = $3) =~ s/_/ /g;
   $device = substr($output, 0, index($output, "\n")+1);
   $output = substr($output, index($output, "\n")+1, length($output));
  }
 }

# Note any duplicate device names and delete if necessary
 my %seen;
 my $i = 0;
 while ($i < @device) {
  $seen{ $device[$i] }++;
  if ($seen{$device[$i]} > 1) {
   splice @device, $i, 1;
   splice @model, $i, 1;
  }
  else {
   $i++;
  }
 }

# Note any duplicate model names and add the device if necessary
 undef %seen;
 foreach ( @model ) {
  $seen{ $_ }++;
 }
 for (my $i = 0; $i < @model; $i++) {
  $model[$i] .= " on $device[$i]" if ($seen{$model[$i]} > 1);
 }

# read the model names into the combobox
 foreach (@model) {
  $combobd->append_text ($_);
 }

# flags whether already run or not
 my $run = FALSE;
 $combobd -> signal_connect (changed => sub {

# only delete the mode setting if switching devices, not on first run
  delete $SETTING{mode} if ($run);
  $run = TRUE;
  rescan_options($vboxd, $device[$combobd -> get_active]);
  $vboxd -> show_all;
  hide_custom();
 });
 $tooltips -> set_tip ($ev, $d->get('Sets the device to be used for the scan'));
 $hboxd -> pack_end ($ev, FALSE, FALSE, 0);

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

# If device in settings then set it
 my $o;
 if (defined $SETTING{device}) {
  for (my $i = 0; $i <= $#device; $i++) {
   $o = $i if ($SETTING{device} eq $device[$i]);
  }
 }
 if (! defined ($o)) {
  $o = 0;
  delete $SETTING{mode};
 }
   
# 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
 $bscanall = Gtk2::RadioButton -> new(undef, $d->get('All'));
 $tooltips -> set_tip ($bscanall, $d->get('Scan all pages'));
 $vboxn -> pack_start($bscanall, TRUE, TRUE, 0);

# Entry button
 my $hboxn = Gtk2::HBox -> new;
 $vboxn -> pack_start($hboxn, TRUE, TRUE, 0);
 $bscannum = Gtk2::RadioButton -> new($bscanall -> get_group, "#:");
 $tooltips -> set_tip ($bscannum, $d->get('Set number of pages to scan'));
 $hboxn -> pack_start($bscannum, 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 {
  $bscannum -> 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') {
   $bscanall -> set_active(TRUE);
  }
  else {
   $bscannum -> set_active(TRUE);
   $spin_button -> set_value($SETTING{'pages to scan'});
  }
 }
 else {
  $bscannum -> set_active(TRUE);
 }

# Set the device dependent devices after the number of pages to scan so that
#  the source button callback can ghost the all button
# This then fires the callback, updating the options, so no need to do it further down.
 $combobd -> set_active($o);

# 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);

# Double sided button
 my $buttond = Gtk2::RadioButton -> new($buttons -> get_group, $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, FALSE, FALSE, 0);

 $ev = Gtk2::EventBox->new;
 my $combobs = Gtk2::ComboBox -> new_text;
 $ev->add($combobs);
 my @side = ($d->get('Facing'), $d->get('Reverse'));
 foreach (@side) {
  $combobs -> append_text ($_);
 }
 $combobs -> signal_connect (changed => sub {
  $buttond -> set_active(TRUE); # Set the radiobutton active
 });
 $tooltips -> set_tip ($ev,
              $d->get('Sets which side of a double-sided document is scanned'));
 $combobs -> set_active(0);
# Have to do this here because setting the facing combobox switches it
 $buttons -> set_active(TRUE);
 $hboxs -> pack_end ($ev, FALSE, FALSE, 0);

# CheckButton for unpaper
 my $hboxu = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxu, FALSE, FALSE, 0);
 my $ubutton = Gtk2::CheckButton -> new($d->get('unpaper scanned pages'));
 $hboxu -> pack_start($ubutton, TRUE, TRUE, 0);
 if (! $dependencies{unpaper}) {
  $ubutton -> set_sensitive(FALSE);
  $ubutton -> set_active(FALSE);
 }
 elsif (defined($SETTING{'unpaper on scan'}) and $SETTING{'unpaper on scan'}) {
  $ubutton -> set_active(TRUE);
 }
 my $button = Gtk2::Button -> new($d->get('Options'));
 $hboxu -> pack_end($button, TRUE, TRUE, 0);
 $button -> signal_connect (clicked => sub {
  my ($windowo, $vbox) = create_window($window, $d->get('unpaper options'), TRUE);
  add_unpaper_options($vbox);

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

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

# Update $SETTING
   get_unpaper_options($unpaper_options);

   $windowo -> destroy;
  });

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

  $windowo -> show_all;
 });

# CheckButton for OCR
 my $hboxo = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxo, FALSE, FALSE, 0);
 my $obutton = Gtk2::CheckButton -> new($d->get('OCR scanned pages'));
 if (! $dependencies{gocr} and ! $dependencies{tesseract}) {
  $hboxo -> set_sensitive(FALSE);
  $obutton -> set_active(FALSE);
 }
 elsif (defined($SETTING{'OCR on scan'}) and $SETTING{'OCR on scan'}) {
  $obutton -> set_active(TRUE);
 }
 $hboxo -> pack_start($obutton, TRUE, TRUE, 0);
 my $comboboxe = combobox_from_array($SETTING{'ocr engine'}, @ocr_engine);
 $hboxo -> pack_end($comboboxe, TRUE, TRUE, 0);
 my ($comboboxl, @tesslang);
 if ($dependencies{tesseract}) {
  (my $hboxl, $comboboxl, @tesslang) = add_ocr_languages($vbox);
  $hboxl->hide_all
   if ($ocr_engine[$comboboxe -> get_active]->[0] ne 'tesseract');
  $comboboxe -> signal_connect (changed => sub {
   if ($ocr_engine[$comboboxe -> get_active]->[0] eq 'tesseract') {
    $hboxl->show_all;
   }
   else {
    $hboxl->hide_all;
   }
  });
  $hboxl->set_sensitive(FALSE) if (! ($obutton -> get_active));
  $obutton -> signal_connect (toggled => sub {
   if ($obutton -> get_active) {
    $hboxl->set_sensitive(TRUE);
   }
   else {
    $hboxl->set_sensitive(FALSE);
   }
  });
 }

# 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);

# 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 {

# Update undo/redo buffers
  take_snapshot();

# Get selected device
  $SETTING{device} = $device[$combobd -> get_active];

# Get device-specific options
  my %options;
  foreach my $hbox ($vboxd -> get_children) {
   my $key;
   if ($hbox -> isa('Gtk2::HBox') and $hbox->sensitive) {
    foreach my $widget ($hbox -> get_children) {
     if ($widget -> isa('Gtk2::Label')) {
      $key = get_key(\%ddo, $widget -> get_label);
     }
     elsif ($widget -> isa('Gtk2::EventBox')) {
      $widget = $widget -> get_child;

# ignore artificial paper size option
      $SETTING{$key} = get_value(\%ddo, $key, $widget -> get_active_text);
      $options{$key} = $SETTING{$key} if ($key ne 'Paper size');
     }
     elsif ($widget -> isa('Gtk2::SpinButton')) {
      $options{$key} = $widget -> get_value;
      $SETTING{$key} = $options{$key};
     }
    }
   }
  }
  print Dumper(\%options) if ($debug);

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

# 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 = 1;
  if ($buttond -> get_active) {
   if (($combobs -> get_active) == 0) { # facing page
    $step = 2;
   }
   else { # reverse page
    if ($start == 1) {
     show_message_dialog($windows, 'error', 'cancel', $d->get('Must scan facing pages first'));
     return TRUE;
    }

    $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 ($bscannum -> get_active) {
     if (($spin_button -> get_value) > $i) {
      show_message_dialog(
       $windows, 'error', 'cancel',
       $d->get("Cannot scan more reverse pages\nthan facing pages")
      );
      return TRUE;
     }
    }

# If user hasn't specified number of pages, then set number that is possible
    else {
     $npages = $i;
    }
   }
  }

  $SETTING{'unpaper on scan'} = $ubutton->get_active;
  $SETTING{'OCR on scan'} = $obutton->get_active;
  if ($SETTING{'OCR on scan'}) {
   $SETTING{'ocr engine'} = $ocr_engine[$comboboxe -> get_active]->[0];
   $SETTING{'ocr language'} = $tesslang[$comboboxl -> get_active]->[0]
    if ($SETTING{'ocr engine'} eq 'tesseract');
  }
  if ($SETTING{'frontend'} eq 'scanimage') {
   scanimage($SETTING{device}, $npages, $start-$step, $step,
                $SETTING{'unpaper on scan'}, $SETTING{'OCR on scan'}, %options);
  }
  else {
   scanadf($SETTING{device}, $npages, $start-$step, $step,
                $SETTING{'unpaper on scan'}, $SETTING{'OCR on scan'}, %options);
  }
 } );

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

# Show window
 $windows -> show_all;
 hide_custom();
}


# Carry out the scan with scanimage and the options passed.

sub scanimage {
 my ($device, $npages, $offset, $step, $unpaper, $ocr, %options) = @_;

 require IPC::Open3;
 require IO::Handle;

# inverted commas needed for strange characters in device name
 $device = "--device-name='$device'";
 if ($npages != 0) {
  $npages = "--batch-count=$npages";
 }
 else {
  $npages = "";
 }

# Device-specific options
 my @options = hash2options(%options);

# Add basic options
 push @options, '--batch';

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

# Create command
 my $cmd = "scanimage $device @options $npages";
 warn "$cmd\n" if $debug;

 if (! @test) {

# flag to ignore error messages after cancelling scan
  my $cancel = FALSE;

# Interface to scanimage
  my ($write, $read);
  my $error = IO::Handle -> new; # this needed because of a bug in open3.
  my $pid = IPC::Open3::open3($write, $read, $error, $cmd);
  warn "Forked PID $pid\n" if ($debug);
  
  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{INT} = 'IGNORE';
   kill INT => $pid;
   $cancel = TRUE;
   undef(@unpaper_stack);
   undef(@ocr_stack);
  });
  $dialog -> show_all;
 
  my $line;
  $scanwatch = Glib::IO->add_watch(fileno($error), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $buffer;
   if ($condition & 'in') { # bit field operation. >= would also work

# Only reading one buffer, rather than until sysread gives EOF because things seem to be strange for stderr
    sysread $error, $buffer, 1024;
    $line .= $buffer;

    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(sprintf($d->get('Scanning page %i...'), $1*$step+$offset));
     }
     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.pnm", $1*$step+$offset, 'Portable anymap',
                                  $SETTING{resolution}, TRUE, $unpaper, $ocr)) {
       $dialog -> destroy;
       show_message_dialog(
        $windows, 'error', 'close', $d->get('Unable to load image')
       );
       return FALSE;
      }
     }
     elsif ($line =~ /^.* Scanner warming up - waiting \d* seconds/) {
      $label -> set_text($d->get('Scanner warming up'));
     }
     elsif ($line =~ /^Scanned page \d*\. \(scanner status = 7\)/) {
      ;
     }
     elsif ($line =~ /^scanimage: sane_start: Document feeder out of documents/) {
      ;
     }
     elsif ($cancel
             and ($line =~ /^scanimage: sane_start: Error during device I\/O/
                  or $line =~ /^scanimage: received signal 2/
                  or $line =~ /^scanimage: trying to stop scanner/)) {
      ;
     }
     elsif ($line =~ /^scanimage: rounded/) {
      warn substr($line, 0, index($line, "\n")+1);
     }
     elsif ($line =~ /^.* sane_start: Device busy/) {
      $dialog -> destroy;
      show_message_dialog($windows, 'error', 'close', $d->get('Device busy'));
     }
     else {
      my $text = $d->get('Unknown message: ')
                                         . substr($line, 0, index($line, "\n"));
warn "$text\n";
      show_message_dialog($windows, 'warning', 'close', $text);
     }
     $line = substr($line, index($line, "\n")+1, length($line));
    }
   }

# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($buffer) or $buffer eq '')) { # bit field operation. >= would also work
    close $read;
    warn 'Waiting to reap process' if ($debug);
    my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
    warn "Reaped PID $pid\n" if ($debug);

# Now finished scanning, set off unpaper or ocr if necessary
    undef $scanwatch;
    if ($unpaper) {
     unpaper_page();
    }
    else {
     ocr_page() if ($ocr);
    }

    $dialog -> destroy;
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  warn "$cmd\n";
 }
}


# Carry out the scan with scanadf and the options passed.

sub scanadf {
 my ($device, $npages, $offset, $step, $unpaper, $ocr, %options) = @_;

 require IPC::Open3;
 require IO::Handle;

# inverted commas needed for strange characters in device name
 $device = "--device-name='$device'";
 my $end;
 if ($npages != 0) {
  $end = "--end-count=$npages";
 }
 else {
  $end = "";
 }
 my $start  = "--start-count=1";

# Device-specific options
 my @options = hash2options(%options);

# Add basic options
 push @options, '-o out%d.pnm';

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

# Create command
 my $cmd = "scanadf $device @options $start $end > /dev/stderr";
 warn "$cmd\n" if $debug;

 if (! @test) {

# Interface to frontend
  my ($write, $read);
  my $error = IO::Handle -> new; # this needed because of a bug in open3.
  my $pid = IPC::Open3::open3($write, $read, $error, $cmd);
  warn "Forked PID $pid\n" if ($debug);
  
  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;
  $scanwatch = Glib::IO->add_watch(fileno($error), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $buffer;
   if ($condition & 'in') { # bit field operation. >= would also work

# Only reading one buffer, rather than until sysread gives EOF because things seem to be strange for stderr
    sysread $error, $buffer, 1024;
    $line .= $buffer;

    while ($line =~ /\n/) {
     if ($line =~ /^.* Scanner warming up - waiting \d* seconds/) {
      $label -> set_text($d->get('Scanner warming up'));
     }
     elsif ($line =~ /^Scanned document out(\d*)\.pnm/) {
      $label -> set_text(sprintf($d->get('Scanned page %i...'), $1*$step+$offset));

# If the scan can't be loaded then blow the scanning dialog away and
# show an error
      if (! import_scan ("out$1.pnm", $1*$step+$offset, 'Portable anymap',
                                  $SETTING{resolution}, TRUE, $unpaper, $ocr)) {
       $dialog -> destroy;
       show_message_dialog(
        $windows, 'error', 'close', $d->get('Unable to load image')
       );
       return FALSE;
      }
     }
     elsif ($line =~ /^Scanned \d* pages/) {
      ;
     }
     elsif ($line =~ /^scanadf: rounded/) {
      warn substr($line, 0, index($line, "\n")+1);
     }
     elsif ($line =~ /^.* sane_start: Device busy/) {
      $dialog -> destroy;
      show_message_dialog($windows, 'error', 'close', $d->get('Device busy'));
     }
     else {
      my $text = $d->get('Unknown message: ')
                                         . substr($line, 0, index($line, "\n"));
warn "$text\n";
      show_message_dialog($windows, 'warning', 'close', $text);
     }
     $line = substr($line, index($line, "\n")+1, length($line));
    }
   }

# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($buffer) or $buffer eq '')) { # bit field operation. >= would also work
    close $read;
    warn 'Waiting to reap process' if ($debug);
    my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
    warn "Reaped PID $pid\n" if ($debug);

# Now finished scanning, set off unpaper or ocr if necessary
    undef $scanwatch;
    if ($unpaper) {
     unpaper_page();
    }
    else {
     ocr_page() if ($ocr);
    }

    $dialog -> destroy;
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  warn "$cmd\n";
 }
}


# Take a hash of options and push them onto an array

sub hash2options {
 my %options = @_;

 my (@options, $key, $value);

# Make sure mode is first in case of mode-dependent options
 if (defined($options{mode})) {
  push @options, "--mode='$options{mode}'";
  delete $options{mode};
 }

 while (($key, $value) = each(%options)) {
  if ($key =~ /^[xylt]$/) {
   push @options, "-$key $value";
  }
  else {
   push @options, "--$key='$value'";
  }
 }
 return @options;
}


# Take new scan and display it

sub import_scan {
 my ($ofilename, $page, $format, $resolution, $delete, $unpaper, $ocr) = @_;
 (my $filename, $resolution) = prepare_import($ofilename, $format, $resolution, $delete);

 my @page = add_image($filename, $page, $resolution);

 if ($unpaper) {
  unpaper_page($ocr, options2unpaper($unpaper_options), @page);
 }
 else {
  ocr_page(@page) if ($ocr);
 }

 return TRUE;
}


sub prepare_import {
 my ($ofilename, $format, $resolution, $delete) = @_;

 warn "Importing $ofilename, format $format\n" if ($debug);

 my %suffix = (
  'Portable Network Graphics'                    => '.png',
  'Joint Photographic Experts Group JFIF format' => '.jpg',
  'Tagged Image File Format'                     => '.tif',
  'Portable anymap'                              => '.pnm',
  'CompuServe graphics interchange format'       => '.gif',
 );

 if (! defined($resolution)) {
  my $image = Image::Magick->new;
  my $x = $image->Read($ofilename);
  warn "$x" if "$x";
  $resolution = $image->Get('x-resolution');
  if ($resolution == 0) {
   $resolution = $image->Get('y-resolution');
   $resolution = 72 if ($resolution == 0); 
  }
 }

 my (undef, $filename) = tempfile(DIR => $dir, SUFFIX => $suffix{$format});
 if (defined($delete) and $delete) {
  system("mv $ofilename $filename");
 }
 else {
  system("cp $ofilename $filename");
 }

 return $filename, $resolution;
}


# Take new scan and display it

sub add_image {
 my ($filename, $page, $resolution) = @_;

# Add to the page list
 $page = $#{$slist -> {data}}+2 if (! defined($page));
 warn "Added $filename at page $page with resolution $resolution\n" if ($debug);

# Block the row-changed signal whilst adding the scan (row) and sorting it.
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 push @{$slist -> {data}}, [ $page, 
                             get_pixbuf($filename, $heightt, $widtht),
                             $filename, undef, $resolution ];
 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] != $page);

 $slist -> select(@page);
 
 update_uimanager();

 return @page;
}


# 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 {

# Update undo/redo buffers
 take_snapshot();

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

# Select nearest page to last current page
 if ($#{$slist->{data}} > -1 and @page) {

# Select just the first one
  @page = ($page[0]);
  $page[0] = $#{$slist->{data}} if ($page[0] > $#{$slist->{data}});
  $slist->select(@page);
 }

 update_uimanager();
}


# Select all scans

sub select_all {
 if ($slist -> has_focus) {
  $slist -> get_selection -> select_all;
 }
 elsif ($textview -> has_focus) {
  my ($start, $end) = $textbuffer->get_bounds;
  $textbuffer->select_range ($start, $end);
 }
}


# Display about dialog

sub about {
 my $about = Gtk2::AboutDialog->new;
# Gtk2::AboutDialog->set_url_hook ($func, $data=undef);
# Gtk2::AboutDialog->set_email_hook ($func, $data=undef);
 $about->set_name ($program);
 $about->set_version ($version);
 my $authors = <<EOS;
John Goerzen
Chris Mayo
David Hampton
Sascha Hunold
EOS
 $about->set_authors ("Jeff Ratcliffe\n\n"
                      .$d->get('Patches gratefully received from:')
		      ."\n$authors");
 $about->set_comments ($d->get('To aid the scan-to-PDF process'));
 $about->set_copyright ($d->get('Copyright 2006--2007 Jeffrey Ratcliffe'));
 $about->set_license ($d->get('Licensed under the GPLv2'));
 $about->set_website ('http://gscan2pdf.sf.net');
 my $translators = <<EOS;
Alexandre Prokoudine
Hugo Pereira
Jen Fraggle
Nicolas Stransky
Petr Jelínek
Piotr Strębski
Alberto Boiti
Dirk Tas
mecedesjorge
Christoph Langner
Chien Cheng Wei
booxter
Rodrigo Donado
Daniel Nylander
Tikkel
Mathieu Goeminne
Andrea (pikkio)
Eric Spierings
Nicolas Velin
EOS
 $about->set_translator_credits ($translators);
 $about->set_artists ('lodp');
 $about->run;
 $about->destroy;
}


# Check that a command exists

sub check_command {
 return system("which $_[0] >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
}


# Rescan device-dependent scan options

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

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

# Stupidly, the different frontends also show different paper sizes, so
# make a device-dependent paper size, starting from the global one
 @paper = @paperg;
 @x = @xg;
 @y = @yg;

 my $output = '';
 if (! @test) {

# Get output from scanimage or scanadf.
# Inverted commas needed for strange characters in device name
  my $cmd = "$SETTING{'frontend'} --help --device-name='$device'";
  $cmd .= " --mode='$SETTING{mode}'" if (defined $SETTING{mode});
  warn "$cmd\n" if $debug;

# Set up ProgressBar
  my $pbar = Gtk2::ProgressBar->new;
  $pbar -> set_pulse_step(.1);
  $pbar->set_text ($d->get('Updating options'));
  $vboxd->pack_start ($pbar, FALSE, FALSE, 0);
  $vboxd -> show_all;
  my $running = TRUE;

# Timer will run until callback returns false 
  my $timer = Glib::Timeout->add (100, sub { if ($running) {
                                              $pbar->pulse;
                                              return TRUE;
                                             }
                                             else {
                                              return FALSE;
                                             } });

# Interface to frontend
  my $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
  warn "Forked PID $pid\n" if ($debug);
 
# Read without blocking
  Glib::IO->add_watch ( fileno($read), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    sysread $read, $line, 1024;
    $output .= $line;
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    close $read;
    warn 'Waiting to reap process' if ($debug);
    my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
    warn "Reaped PID $pid\n" if ($debug);
    $running = FALSE;
    $pbar -> destroy;
    $vboxd -> hide_all;
    warn $output if $debug;
    parse_options($vboxd, $device, $output);
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  my $i = 0;
  $i++ while ($device[$i] ne $device);

# check if we have output for the mode
  my $filename = $test[$i];
  $filename .= ".$SETTING{mode}"
   if (defined($SETTING{mode}) and -e "$filename.$SETTING{mode}");

# Slurp it from file
  $output = slurp($filename);
  $vboxd -> hide_all; # merely here for consistency with normal operation
  parse_options($vboxd, $device, $output);
 }
}


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

# 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, $l, $t);
 $x = $2 if ($output =~ /-x (auto\|)?\d*\.?\d*\.\.(\d*\.?\d*)/);
 $y = $2 if ($output =~ /-y (auto\|)?\d*\.?\d*\.\.(\d*\.?\d*)/);
 $l = $2 if ($output =~ /-l (auto\|)?\d*\.?\d*\.\.(\d*\.?\d*)/);
 $t = $2 if ($output =~ /-t (auto\|)?\d*\.?\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 $ev = Gtk2::EventBox->new;
  $combobp = Gtk2::ComboBox -> new_text;
  $ev->add($combobp);

# 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 combobox if scanner large enough
# can't use "for" because of the splices
  my $i = 0;
  while ($i <= $#paper) {
   if ($x+$tolerance >= $x[$i] and $y+$tolerance >= $y[$i]) {
    $combobp -> append_text ($d->get($paper[$i]));
    ++$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
  $combobp -> signal_connect (changed => sub {
   if ($combobp -> get_active_text eq $d->get('Custom')) {
    $hboxc -> show_all;
   }
   else {
    my $i = $combobp -> get_active;
    $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
   }
  });
  $tooltips -> set_tip ($ev,
                $d->get('Selects the paper size, e.g. A4, Letter, or Custom'));
  $hboxp -> pack_end ($ev, FALSE, FALSE, 0);

# Set default paper size from config
  my $o = 0;
  if (defined($SETTING{'Paper size'})) {
   $i = 0;
   while ($i <= $#paper) {
    $o = $i if ($paper[$i] eq $SETTING{'Paper size'});
    ++$i;
   }
  }
  $combobp -> set_active($o);

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

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

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

# Origin
  my $hboxo = Gtk2::HBox -> new;
  my $hboxol = Gtk2::HBox -> new;
  my $labelo = Gtk2::Label -> new ($d->get('Top-left position of scan area'));
  $hboxol -> pack_start ($labelo, FALSE, FALSE, 0);
  $vboxd -> pack_start ($hboxol, TRUE, TRUE, 0);
  my $labell = Gtk2::Label -> new ('l');
  my $spin_buttonl = Gtk2::SpinButton -> new_with_range(0, $l, 1);
  $spin_buttonl -> set_value($SETTING{'l'});
  my $labelt = Gtk2::Label -> new ('t');
  my $spin_buttont = Gtk2::SpinButton -> new_with_range(0, $t, 1);
  $spin_buttont -> set_value($SETTING{'t'});
  $tooltips -> set_tip ($spin_buttonl, $d->get('Top-left x position of scan area'));
  $tooltips -> set_tip ($spin_buttont, $d->get('Top-left y position of scan area'));
  $hboxo -> pack_end ($spin_buttont, FALSE, FALSE, 0);
  $hboxo -> pack_end ($labelt, FALSE, FALSE, 0);
  $hboxo -> pack_end ($spin_buttonl, FALSE, FALSE, 0);
  $hboxo -> pack_end ($labell, FALSE, FALSE, 0);
  $vboxd -> pack_start ($hboxo, FALSE, FALSE, 0);

  $spin_buttonx -> signal_connect ('value-changed' => sub {
   my ($minx, $maxx) = $spin_buttonx -> get_range;
   $spin_buttonl -> set_range(0, $maxx-$spin_buttonx->get_value);
  });
  $spin_buttony -> signal_connect ('value-changed' => sub {
   my ($miny, $maxy) = $spin_buttony -> get_range;
   $spin_buttont -> set_range(0, $maxy-$spin_buttony->get_value);
  });

# Set scan area from config if available, thus triggering updates for l & t
  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 = $combobp -> get_active;
   $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);
  }
 }

# Set device-dependent options
# Dummy entries so that something is returned:
 %ddo = (
          'Paper size' => { string => $d->get('Paper size'),
                            values => {
                                      'A4' => $d->get('A4'),
                                      'US Letter'  => $d->get('US Letter'),
                                      'US Legal'  => $d->get('US Legal'),
                                      'Custom'  => $d->get('Custom'),
                                      } },
          'x'          => { string => 'x' },
          'y'          => { string => 'y' },
          'Origin'     => { string => $d->get('Top-left position of scan area'), },
          'l'          => { string => 'l' },
          't'          => { string => 't' },
        );

# Add remaining options
 my %hash = options2hash($output);
 foreach my $option (keys %hash) {
  if (defined($pddo{$option})) {

# Dig out of possible options
   $ddo{$option}{string} = $pddo{$option}{string};

# HBox for option
   my $hbox = Gtk2::HBox -> new;
   $vboxd -> pack_start ($hbox, TRUE, TRUE, 0);
   $hbox->set_sensitive(FALSE) if ($hash{$option}{default} =~ /inactive/);

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

# SpinButton
   if ($hash{$option}{values} =~ /(-?\d*\.?\d*)\.\.(\d*\.?\d*)/) {
    my $min = $1;
    my $max = $2;
    my $step = 1;
    $step = $1 if ($hash{$option}{values} =~ /\(in steps of (\d+)\)/);
    my $spin_button = Gtk2::SpinButton -> new_with_range($min, $max, $step);
    $spin_button -> set_value($hash{$option}{default})
     if ($hash{$option}{default} !~ /inactive/);
    $hbox -> pack_end ($spin_button, FALSE, FALSE, 0);
    $tooltips -> set_tip ($spin_button, $hash{$option}{tip});

# Set the default
    $spin_button -> set_value($hash{$option}{default})
     if ($hash{$option}{default} !~ /inactive/);
   }

# ComboBox
   else {
    my $ev = Gtk2::EventBox->new;
    my $combob = Gtk2::ComboBox -> new_text;
    $ev->add($combob);

    my @array = options2array($hash{$option}{values});
    my $index = default2index($hash{$option}{default}, @array);
    foreach (@array) {
     add_to_options($option, $_);
     $combob->append_text($ddo{$option}{values}{$_});
    }

# Doing this before the set_active call to make sure that it gets calls for the
#  default setting
# If an ADF isn't selected, then we don't want to scan all pages
    if ($option eq 'source') {
     $combob -> signal_connect (changed => sub {
      if (get_value(\%ddo, $option, $combob -> get_active_text)
                                                      =~ /(Flatbed)|(Normal)/) {
       $bscanall->set_sensitive(FALSE);
       $bscannum->set_active(TRUE);
      }
      else {
       $bscanall->set_sensitive(TRUE);
      }
     });
    }

# Set the default
    $combob -> set_active($index) if (defined $index);

# Doing this after the set_active call so that it doesn't get called twice.
# callback for mode
    if ($option eq 'mode') {
     $combob -> signal_connect (changed => sub {
      $SETTING{$option} = get_value(\%ddo, $option, $combob -> get_active_text);
      update_options($vboxd, $device);
     });
    }
    
    $hbox -> pack_end ($ev, FALSE, FALSE, 0);
    $tooltips -> set_tip ($ev, $hash{$option}{tip});
   }
  }
 }

# Show window, hiding the custom hbox and sizing if necessary
 $vboxd -> show_all;
 hide_custom();
}


# Update device-dependent scan options having selected a new mode

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

# Empty $vboxd first
 foreach ($vboxd -> get_children) {
  $_ -> hide;
 }

 my $output = '';
 if (! @test) {

# Get output from scanimage or scanadf.
# Inverted commas needed for strange characters in device name
  my $cmd = "$SETTING{'frontend'} --help --device-name='$device'";
  $cmd .= " --mode='$SETTING{mode}'" if (defined $SETTING{mode});
  warn "$cmd\n" if $debug;

# Set up ProgressBar
  my $pbar = Gtk2::ProgressBar->new;
  $pbar -> set_pulse_step(.1);
  $pbar->set_text ($d->get('Updating options'));
  $vboxd->pack_start ($pbar, FALSE, FALSE, 0);
  $pbar -> show;
  my $running = TRUE;

# Timer will run until callback returns false 
  my $timer = Glib::Timeout->add (100, sub { if ($running) {
                                              $pbar->pulse;
                                              return TRUE;
                                             }
                                             else {
                                              return FALSE;
                                             } });

# Interface to frontend
  my $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
  warn "Forked PID $pid\n" if ($debug);
 
# Read without blocking
  Glib::IO->add_watch ( fileno($read), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    sysread $read, $line, 1024;
    $output .= $line;
   }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
   if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
    close $read;
    warn 'Waiting to reap process' if ($debug);
    my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
    warn "Reaped PID $pid\n" if ($debug);
    $running = FALSE;
    $pbar -> destroy;
    $vboxd -> hide_all;
    warn $output if $debug;
    update_options_hash($vboxd, options2hash($output));
    $vboxd -> show_all;
    hide_custom();
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  my $i = 0;
  $i++ while ($device[$i] ne $device);

# check if we have output for the new mode
  my $filename = $test[$i];
  $filename .= ".$SETTING{mode}" if (-e "$filename.$SETTING{mode}");

# Slurp it from file
  $output = slurp($filename);
  $vboxd -> hide_all; # merely here for consistency with normal operation
  update_options_hash($vboxd, options2hash($output));
  $vboxd -> show_all;
  hide_custom();
 }
}


# return a hash of the passed options

sub options2hash {

 my ($output) = @_;
 my %hash = Gscan2pdf::options2hash($output);
 print Dumper(\%hash) if ($debug);
 foreach (keys %hash) {

# Set default from config
  if (defined($SETTING{$_}) and $hash{$_}{default} ne 'inactive'

# only set the default if it is an option
                           and $hash{$_}{values} =~ /($SETTING{$_})|(\.\.)/) {
   $hash{$_}{default} = $SETTING{$_};
  }

# in the case that we've switched devices and the new device doesn't support
# the new mode
  elsif ($_ eq 'mode') {
   delete $SETTING{$_};
  }
 }
 return %hash;
}


# walk the widget tree and update them from the hash

sub update_options_hash {

 my ($vboxd, %hash) = @_;

 foreach my $hbox ($vboxd -> get_children) {
  my $key;
  if ($hbox -> isa('Gtk2::HBox')) {
   foreach my $widget ($hbox -> get_children) {
    if ($widget -> isa('Gtk2::Label')) {
     $key = get_key(\%ddo, $widget -> get_label);
    }
    elsif ($widget -> isa('Gtk2::EventBox')
# to prevent recursion
                                            and $key ne 'mode'
                                            and defined($hash{$key}{values})
                                            and $hash{$key}{values} !~ /\.\./) {
     $widget = $widget -> get_child;
     
# Empty the list
     $widget->get_model->clear;

# Fill it again
     my @array = options2array($hash{$key}{values});
     my $index = default2index($hash{$key}{default}, @array);
     foreach (@array) {

# in case the option was new and therefore not in ddo before
      add_to_options($key, $_);
      $widget->append_text($ddo{$key}{values}{$_});
     }

# Set the default
     $widget -> set_active($index) if (defined $index);
    }
    elsif ($widget -> isa('Gtk2::SpinButton') and defined($hash{$key}{values})
                                            and $hash{$key}{values} =~ /\.\./) {
     $widget->set_range($1, $2)
      if ($hash{$key}{values} =~ /(-?\d*\.?\d*)\.\.(\d*\.?\d*)/);

# Set the default
     $widget -> set_value($hash{$key}{default})
      if ($hash{$key}{default} !~ /inactive/);
    }
   }

# Update ghosting
   if (defined($hash{$key}{default}) and $hash{$key}{default} =~ /inactive/) {
    $hbox->set_sensitive(FALSE);
   }
   else {
    $hbox->set_sensitive(TRUE);
   }    
  }
 }
}


# take a list of |-seperated options and return an array

sub options2array {

 my ($options) = @_;
 my @array;
 while (defined $options) {
  my $i = index($options, '|');
  if ($i > -1) {
   push @array, substr($options, 0, $i);
   $options = substr($options, $i+1, length($options));
  }
  else {
   push @array, $options;
   undef $options;
  }
 }
 return @array;
}


# return the position that a string occurs in an array

sub default2index {

 my ($default, @array) = @_;
 for (my $i = 0; $i <= $#array; $i++) {
  return $i if ($array[$i] eq $default);
 }
 return undef;
}


# Hides the custom paper size on scan dialog if not necessary

sub hide_custom {
 if (! defined($combobp) or ($combobp->get_active_text ne $d->get('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
 }
}


# Renumber pages

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

# Update undo/redo buffers
 take_snapshot();

 $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) = @_;

# Update undo/redo buffers
 take_snapshot();

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

# Rotate the tiff with imagemagick
  my $image = Image::Magick->new;
  my $x = $image->Read($slist -> {data}[$page[$i]][2]);
  warn "$x" if "$x";

# workaround for those versions of imagemagick that produce 16bit output
# with rotate
  my $depth = $image->Get('depth');
  $x = $image->Rotate($degrees);
  warn "$x" if "$x";
  $x = $image->Write(filename => $slist -> {data}[$page[$i]][2], depth => $depth);
  warn "$x" if "$x";

# 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) = @_;

# $SETTING{'RMB'} = ($event->button == 3);
#warn "rmb $SETTING{'RMB'}\n";

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

 my $popup_menu;
 if ($widget->isa('Gtk2::EventBox'))  { # main image
  $popup_menu = $uimanager->get_widget('/Detail_Popup');
 }
 else { # Thumbnail simplelist
  $popup_menu = $uimanager->get_widget('/Thumb_Popup');
 }

 $popup_menu->show_all;
 $popup_menu->popup(undef, undef,
                    undef, undef,
                    $event->button,
                    $event->time);

 # block event propagation
 return TRUE;
}


# guess from which window the sub was called

sub get_parent {
 my ($w1, $w2, $w3) = @_;
 if (defined($w1) and $w1->visible) {
  return $w1;
 }
 elsif (defined($w2) and $w2->visible) {
  return $w2;
 }
 else {
  return $w3;
 }
}


# Add $page to the unpaper stack, setting it off if not running.

sub unpaper_page {
 my $ocr = shift;
 my $options = shift;
 $options = '' if (! defined($options));
 push @unpaper_stack, [ $_, $ocr, $options ] foreach (@_);

# guess where unpaper has been called from
 my $parent = get_parent($windowu, $windows, $window);

# don't run ocr and unpaper concurrently
 if (! defined($scanwatch) and ! defined($unpaper_timer) and ! defined($ocr_timer)) {
  my $dialog = Gtk2::Dialog -> new ($d->get('Running unpaper')."...", $parent,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');
  my $label = Gtk2::Label -> new ($d->get('Running unpaper')."...");
  $dialog -> vbox -> add($label);

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

# Flag set if unpaper is running
  my $running = FALSE;
  my $cancelled = FALSE;
  my $page = 0;
  my $npages = 0;

# Ensure that the dialog box is destroyed when the user responds.
  my $pid;
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   local $SIG{HUP} = 'IGNORE';
   kill HUP => $pid;
   $cancelled = TRUE;
   undef(@unpaper_stack);
  });
  $dialog -> show_all;

# Timer will run until callback returns false 
  $unpaper_timer = Glib::Timeout->add (100, sub {

# To prevent any further pages being processed
   if ($cancelled) {
    undef $unpaper_timer;
    return FALSE;  # uninstall
   }
   elsif (@unpaper_stack) {
    if (! $running) {
     $running = TRUE;
     $page++;
     $npages++;
     my ($pagenum, $ocr, $options) = @{shift @unpaper_stack};
     $pbar->set_text(sprintf($d->get("Page %i of %i"), $page, $#unpaper_stack+$npages+1));
     $pbar->set_fraction (($page-1)/($#unpaper_stack+$npages+1));

     my $cmd = '';
     my $in;
     my (undef, $out) = tempfile(DIR => $dir, SUFFIX => '.pnm');

     if ($slist -> {data}[$pagenum][2] !~ /\.pnm$/) {
      (undef, $in) = tempfile(DIR => $dir, SUFFIX => '.pnm');
      $cmd .= "convert -compress Zip $slist->{data}[$pagenum][2] $in;";
     }
     else {
      $in = $slist -> {data}[$pagenum][2];
     }

# --overwrite needed because $out exists with 0 size
     $cmd .= "unpaper $options --overwrite $in $out;";
     $cmd .= "rm $in" if ($slist -> {data}[$pagenum][2] !~ /\.pnm$/);
     warn "$cmd\n" if ($debug);

# Interface to unpaper
     $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
     warn "Forked PID $pid\n" if ($debug);
     my $page_buffer = '';
 
# Update TextBuffer without blocking
     Glib::IO->add_watch ( fileno($read), ['in', 'hup'], sub {
      my ($fileno, $condition) = @_;
      my $line;
      if ($condition & 'in') { # bit field operation. >= would also work
       sysread $read, $line, 1024;
       $page_buffer .= $line;
      }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
      if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
       warn $page_buffer if ($debug);
       close $read;
       warn 'Waiting to reap process' if ($debug);
       my $pid = waitpid(-1, &WNOHANG);
       warn "Reaped PID $pid\n" if ($debug);

# Note page selection
       my @selection = $slist -> get_selected_indices;

       $slist -> get_model -> signal_handler_block($slist -> {signalid});
       $slist -> {data}[$pagenum][2] = $out;
       $slist -> {data}[$pagenum][1] = get_pixbuf($out, $heightt, $widtht);
       $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Reselect selected pages, firing the display callback
       $slist -> get_selection -> unselect_all;
       $slist -> select(@selection);

       $running = FALSE;
       ocr_page($pagenum) if ($ocr);

       return FALSE;  # uninstall
      }
      return TRUE;  # continue without uninstalling
     });
    }
    return TRUE;  # continue without uninstalling
   }
   elsif (! $running) {
    $dialog -> destroy;
    undef $unpaper_timer;

# set off ocr if necessary now that unpaper has finished
    ocr_page() if (@ocr_stack);
    return FALSE;  # uninstall
   }
   else {
    return TRUE;  # continue without uninstalling
   }
  });
 }
}


sub add_widget {
 my ($vbox, $hashref, $option) = @_;

 my $widget;
 $SETTING{$option} = $hashref->{$option}{default}
  if (defined($hashref->{$option}{default}) and ! defined($SETTING{$option}));

 if ($hashref->{$option}{type} eq 'ComboBox') {
  my $hbox = Gtk2::HBox -> new;
  $vbox -> pack_start($hbox, TRUE, TRUE, 0);
  my $label = Gtk2::Label -> new ($hashref->{$option}{string});
  $hbox -> pack_start ($label, FALSE, FALSE, 0);
  $widget = Gtk2::EventBox->new;
  my $combobox = Gtk2::ComboBox->new_text;
  $widget->add($combobox);
  $hbox -> pack_end ($widget, FALSE, FALSE, 0);

# Add text and tooltips
  my @tooltip;
  my $i = -1;
  my $o = 0;
  foreach (keys %{$hashref->{$option}{options}}) {
   $combobox -> append_text ($hashref->{$option}{options}{$_}{string});
   push @tooltip, $hashref->{$option}{options}{$_}{tooltip};
   $hashref->{$option}{options}{$_}{index} = ++$i;
   $o = $i if ($_ eq $SETTING{$option});
  }
  $combobox -> signal_connect (changed => sub {
   $tooltips -> set_tip ($widget, $tooltip[$combobox->get_active])
    if (defined $tooltip[$combobox->get_active]);
  });

# Set defaults
  $combobox -> set_active ($o);
  $tooltips -> set_tip ($widget, $tooltip[0]);
 }

 elsif ($hashref->{$option}{type} eq 'CheckButton') {
  $widget = Gtk2::CheckButton -> new($hashref->{$option}{string});
  $tooltips -> set_tip ($widget, $hashref->{$option}{tooltip});
  $vbox -> pack_start ($widget, TRUE, TRUE, 0);
  $widget->set_active ($SETTING{$option}) if defined($SETTING{$option});
 }

 elsif ($hashref->{$option}{type} eq 'CheckButtonGroup') {
  $widget = Gtk2::Frame -> new($hashref->{$option}{string});
  $vbox -> pack_start ($widget, TRUE, TRUE, 0);
  my $vboxf = Gtk2::VBox -> new;
  $vboxf -> set_border_width($border_width);
  $widget -> add ($vboxf);
  $tooltips -> set_tip ($widget, $hashref->{$option}{tooltip});
  my %default;
  if (defined $SETTING{$option}) {
   foreach (split /,/, $SETTING{$option}) {
    $default{$_} = TRUE;
   }
  }
  foreach (keys %{$hashref->{$option}{options}}) {
   my $button = add_widget($vboxf, $hashref->{$option}{options}, $_);
   $button->set_active (TRUE) if (defined $default{$_});
  }
 }

 elsif ($hashref->{$option}{type} eq 'SpinButton') {
  my $hbox = Gtk2::HBox -> new;
  $vbox -> pack_start ($hbox, TRUE, TRUE, 0);
  my $label = Gtk2::Label -> new ($hashref->{$option}{string});
  $hbox -> pack_start ($label, FALSE, FALSE, 0);
  $widget = Gtk2::SpinButton -> new_with_range($hashref->{$option}{min}, $hashref->{$option}{max}, $hashref->{$option}{step});
  $hbox -> pack_end ($widget, FALSE, FALSE, 0);
  $tooltips -> set_tip ($widget, $hashref->{$option}{tooltip});
  $widget->set_value ($SETTING{$option}) if (defined $SETTING{$option});
 }

 elsif ($hashref->{$option}{type} eq 'SpinButtonGroup') {
  $widget = Gtk2::Frame -> new($hashref->{$option}{string});
  $vbox -> pack_start ($widget, TRUE, TRUE, 0);
  my $vboxf = Gtk2::VBox -> new;
  $vboxf -> set_border_width($border_width);
  $widget -> add ($vboxf);
  my @default = split /,/, $SETTING{$option} if (defined $SETTING{$option});
  foreach (keys %{$hashref->{$option}{options}}) {
   my $button = add_widget($vboxf, $hashref->{$option}{options}, $_);
   $button->set_value (shift @default) if (@default);
  }
 }

 $hashref->{$option}{widget} = $widget;
 return $widget;
}


sub get_unpaper_options {
 my $hashref = shift;

 foreach my $option (keys %{$hashref}) {
  if ($hashref->{$option}{type} eq 'ComboBox') {
   my $i = $hashref->{$option}{widget} -> get_child -> get_active;
   my $item;
   foreach (keys %{$hashref->{$option}{options}}) {
    $item = $_ if ($hashref->{$option}{options}{$_}{index} == $i);
   }
   if (defined $item) {
    $SETTING{$option} = $item;
   }
   elsif (defined $SETTING{$option}) {
    delete $SETTING{$option};
   }
  }
  elsif ($hashref->{$option}{type} eq 'CheckButton') {
   if ($hashref->{$option}{widget} -> get_active) {
    $SETTING{$option} = TRUE;
   }
   else {
    delete $SETTING{$option};
   }
  }
  elsif ($hashref->{$option}{type} eq 'SpinButton') {
   $SETTING{$option} = $hashref->{$option}{widget} -> get_value;
  }
  elsif ($hashref->{$option}{type} eq 'CheckButtonGroup') {
   my @items;
   foreach (keys %{$hashref->{$option}{options}}) {
    push @items, $_ if ($hashref->{$option}{options}{$_}{widget} -> get_active);
   }
   if (@items) {
    $SETTING{$option} = join ',', @items;
   }
   elsif (defined $SETTING{$option}) {
    delete $SETTING{$option};
   }
  }
 }
}


sub options2unpaper {
 my $hashref = $unpaper_options;

 my @items;
 foreach my $option (keys %{$hashref}) {
  if ($hashref->{$option}{type} eq 'CheckButton') {
   push @items, "--$option"
    if (defined $SETTING{$option} and $SETTING{$option});
  }
  else {
   push @items, "--$option $SETTING{$option}" if (defined $SETTING{$option});
  }
 }
 return join ' ', @items;
}


sub add_unpaper_options {
 my ($vbox) = @_;

# Layout ComboBox
 my $combobl = add_widget($vbox, $unpaper_options, 'layout') -> get_child;

# Notebook to collate options
 my $notebook = Gtk2::Notebook->new;
 $vbox->pack_start($notebook, TRUE, TRUE, 0);

# Notebook page 1
 my $vbox1 = Gtk2::VBox->new;
 $notebook->append_page($vbox1, $d->get('Deskew'));

 my $dsbutton = add_widget($vbox1, $unpaper_options, 'no-deskew');

# Frame for Deskew Scan Direction
 my $dframe = add_widget($vbox1, $unpaper_options, 'deskew-scan-direction');
 $dsbutton -> signal_connect (toggled => sub {
  if ($dsbutton -> get_active) {
   $dframe->set_sensitive(FALSE);
  }
  else {
   $dframe->set_sensitive(TRUE);
  }
 });

 foreach (keys %{$unpaper_options->{'deskew-scan-direction'}{options}}) {
  my $button = $unpaper_options->{'deskew-scan-direction'}{options}{$_}{widget};

# Ensure that at least one checkbutton stays active
  $button -> signal_connect (toggled => sub {
   my $n = 0;
   foreach ($dframe -> get_child -> get_children) {
    $n++ if ($_ -> get_active);
   }
   $button->set_active(TRUE) if ($n == 0);
  });
 }

# Notebook page 2
 my $vbox2 = Gtk2::VBox->new;
 $notebook->append_page($vbox2, $d->get('Border'));

 my $bsbutton = add_widget($vbox2, $unpaper_options, 'no-border-scan');
 my $babutton = add_widget($vbox2, $unpaper_options, 'no-border-align');

# Frame for Align Border
 my $bframe = add_widget($vbox2, $unpaper_options, 'border-align');
 $bsbutton -> signal_connect (toggled => sub {
  if ($bsbutton -> get_active) {
   $bframe->set_sensitive(FALSE);
   $babutton->set_sensitive(FALSE);
  }
  else {
   $babutton->set_sensitive(TRUE);
   $bframe->set_sensitive(TRUE) if (! ($babutton -> get_active));
  }
 });
 $babutton -> signal_connect (toggled => sub {
  if ($babutton -> get_active) {
   $bframe->set_sensitive(FALSE);
  }
  else {
   $bframe->set_sensitive(TRUE);
  }
 });

# Define margins here to reference them below
 my $bmframe = add_widget($vbox2, $unpaper_options, 'border-margin');
 $bmframe->set_sensitive(FALSE);

 my $vboxb = $bframe -> get_child;
 foreach (keys %{$unpaper_options->{'border-align'}{options}}) {
  my $button = $unpaper_options->{'border-align'}{options}{$_}{widget};

# Ghost margin if nothing selected
  $button -> signal_connect (toggled => sub {
   my $n = 0;
   foreach ($vboxb -> get_children) {
    $n++ if ($_ -> get_active);
   }
   if ($n == 0) {
    $bmframe->set_sensitive(FALSE);
   }
   else {
    $bmframe->set_sensitive(TRUE);
   }
  });
 }

# Notebook page 3
 my $vbox3 = Gtk2::VBox->new;
 $notebook->append_page($vbox3, $d->get('Filters'));

 my $spinbuttonwt = add_widget($vbox3, $unpaper_options, 'white-threshold');
 my $spinbuttonbt = add_widget($vbox3, $unpaper_options, 'black-threshold');
 my $msbutton = add_widget($vbox3, $unpaper_options, 'no-mask-scan');
 my $bfbutton = add_widget($vbox3, $unpaper_options, 'no-blackfilter');
 my $gfbutton = add_widget($vbox3, $unpaper_options, 'no-grayfilter');
 my $nfbutton = add_widget($vbox3, $unpaper_options, 'no-noisefilter');
 my $blbutton = add_widget($vbox3, $unpaper_options, 'no-blurfilter');
}


# Run unpaper to clean up scan.

sub unpaper {

 if (defined $windowu) {
  $windowu -> present;
  return;
 }

 ($windowu, my $vbox) = create_window($window, $d->get('unpaper'), FALSE);
 add_unpaper_options($vbox);

# Frame for page range
 my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

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

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

# Update undo/redo buffers
  take_snapshot();

# Update $SETTING
  get_unpaper_options($unpaper_options);

# run unpaper
  unpaper_page(undef, options2unpaper($unpaper_options), get_page_index());
 } );

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

 $windowu -> show_all;
}


# Add $page to the OCR stack, setting it off if not running.

sub ocr_page {
 push @ocr_stack, @_;

# guess where ocr has been called from
 my $parent = get_parent($windowo, $windows, $window);

# don't run ocr and unpaper concurrently
 if (! defined($scanwatch) and ! defined($unpaper_timer) and ! defined($ocr_timer)) {
  my $dialog = Gtk2::Dialog -> new ($d->get('Running OCR')."...", $parent,
                                    'destroy-with-parent',
                                    'gtk-cancel' => 'cancel');
  my $label = Gtk2::Label -> new ($d->get('Running OCR')."...");
  $dialog -> vbox -> add($label);

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

# Flag set if ocr is running
  my $running = FALSE;
  my $cancelled = FALSE;
  my $page = 0;
  my $npages = 0;

# Ensure that the dialog box is destroyed when the user responds.
  my $pid;
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   local $SIG{HUP} = 'IGNORE';
   kill HUP => $pid;
   $cancelled = TRUE;
   undef(@ocr_stack);
  });
  $dialog -> show_all;

# Timer will run until callback returns false 
  $ocr_timer = Glib::Timeout->add (100, sub {

# To prevent any further pages being processed
   if ($cancelled) {
    undef $ocr_timer;
    return FALSE;  # uninstall
   }
   elsif (@ocr_stack) {
    if (! $running) {
     $running = TRUE;
     $page++;
     $npages++;
     my $pagenum = shift @ocr_stack;
     $pbar->set_text(sprintf($d->get("Page %i of %i"), $page, $#ocr_stack+$npages+1));
     $pbar->set_fraction (($page-1)/($#ocr_stack+$npages+1));

# Temporary filename for output
     my (undef, $txt) = tempfile(DIR => $dir);

     my $cmd;
     if (defined($SETTING{'ocr engine'}) and $SETTING{'ocr engine'} eq 'gocr') {
      my $pnm;
      if ($slist -> {data}[$pagenum][2] !~ /\.pnm$/) {
       my $file = $slist -> {data}[$pagenum][2];

# Temporary filename for new file
       (undef, $pnm) = tempfile(DIR => $dir, SUFFIX => '.pnm');

       $cmd = "convert $file $pnm; gocr $pnm > $txt.txt; rm $pnm";
      }
      else {
       $pnm = $slist -> {data}[$pagenum][2];
       $cmd .= "gocr $pnm > $txt.txt;";
      }
     }
     else {
      my ($tif, $pre, $post);
      if ($slist -> {data}[$pagenum][2] !~ /\.tif$/) {
       my $file = $slist -> {data}[$pagenum][2];

# Temporary filename for new file
       (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');
       $pre = "convert $file $tif;";
       $post = "; rm $tif";

      }
      else {
       $tif = $slist -> {data}[$pagenum][2];
       $pre = '';
       $post = '';
      }
      if (defined $SETTING{'ocr language'}) {
       $cmd = "$pre tesseract $tif $txt -l $SETTING{'ocr language'}$post";
      }
      else {
       $cmd = "$pre tesseract $tif $txt$post";
      }
     }
     warn "$cmd\n" if ($debug);

# Interface to ocr engine
     $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
     warn "Forked PID $pid\n" if ($debug);
 
# Update TextBuffer without blocking
     Glib::IO->add_watch ( fileno($read), ['in', 'hup'], sub {
      my ($fileno, $condition) = @_;
      my $line;
      if ($condition & 'in') { # bit field operation. >= would also work
       sysread $read, $line, 1024;
      }

# Can't have elsif here because of the possibility that both in and hup are set.
# Only allow the hup if sure an empty buffer has been read.
      if (($condition & 'hup') and (! defined($line) or $line eq '')) { # bit field operation. >= would also work
       close $read;
       warn 'Waiting to reap process' if ($debug);
       my $pid = waitpid(-1, &WNOHANG); # So we don't leave zombies
       warn "Reaped PID $pid\n" if ($debug);
       $running = FALSE;

# Doing this once at the end to avoid firing the row-changed signal too often
       $slist -> get_model -> signal_handler_block($slist -> {signalid});

# Slurp the OCR output
       $slist -> {data}[$pagenum][3] .= slurp("$txt.txt");
       unlink <$txt*>;
       $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Update the buffer if current
       my @page = $slist -> get_selected_indices;
       if ($page[0] == $pagenum) {
        $textbuffer -> signal_handler_block($textbuffer -> {signalid});
        $textbuffer -> set_text ($slist -> {data}[$pagenum][3]);
        $textbuffer -> signal_handler_unblock($textbuffer -> {signalid});
       }
       return FALSE;  # uninstall
      }
      return TRUE;  # continue without uninstalling
     });
    }
    return TRUE;  # continue without uninstalling
   }
   elsif (! $running) {
    $dialog -> destroy;
    undef $ocr_timer;

# start unpaper if necessary now that ocr has finished
    unpaper_page() if (@unpaper_stack);
    return FALSE;  # uninstall
   }
   else {
    return TRUE;  # continue without uninstalling
   }
  });
 }
}


# Have to roll my own slurp sub to support utf8

sub slurp {
 my ($file) = @_;

 local( $/, *FH );
 open( FH, "<:utf8", $file ) or die "Error: cannot open $file\n";
 my $text = <FH>;
 return $text;
}


# Create a combobox from an array and set the default

sub combobox_from_array {
 my ($default, @array) = @_;

# Fill ComboBox
 my $i = 0;
 my $o;
 my $combobox = Gtk2::ComboBox->new_text;
 foreach ( @array ) {
  $combobox->append_text ($_->[1]);

  $o = $i if (defined($default) and defined($_->[0]) and $_->[0] eq $default);
  ++$i;
 }
 $o = 0 if (! defined $o);
 $combobox -> set_active ($o);
 
 return $combobox;
}


# Add hbox for ocr languages

sub add_ocr_languages {
 my ($vbox) = @_;

 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start ($hbox, FALSE, FALSE, 0);
 my $label = Gtk2::Label -> new ($d->get('Language to recognise'));
 $hbox -> pack_start($label, TRUE, TRUE, 0);

# Tesseract language files
 my %iso639 = (
  deu => $d->get('German'),
  eng => $d->get('English'),
  fra => $d->get('French'),
  ita => $d->get('Italian'),
  nld => $d->get('Dutch'),
  spa => $d->get('Spanish'),
 );

 my @tesslang;
 if (defined $ENV{TESSDATA_PREFIX}) {
  @tesslang = glob "$ENV{TESSDATA_PREFIX}tessdata/*.unicharset";
 }
 else {
  @tesslang = glob "/usr/share/tesseract-ocr/tessdata/*.unicharset";
 }
  
# Weed out the empty language files
 my $i = 0;
 while ($i < @tesslang) {
  if (-z $tesslang[$i]) {
   splice @tesslang, $i, 1;
  }
  else {
   my $code = $1 if ($tesslang[$i] =~ /(\w*)\.unicharset$/);
   if (defined $iso639{$code}) {
    $tesslang[$i] = [ $code, $iso639{$code} ];
   }
   else {
    $tesslang[$i] = [ $code, $code ];
   }
   $i++;
  }
 }

# If there are no language files, then we have tesseract-1.0, i.e. English
 push @tesslang, [ undef, $d->get('English') ] if (! @tesslang);

 my $combobox = combobox_from_array($SETTING{'ocr language'}, @tesslang);
 $hbox -> pack_end($combobox, TRUE, TRUE, 0);
 return $hbox, $combobox, @tesslang;
}


# Run OCR on current page and display result

sub OCR {

 if (defined $windowo) {
  $windowo -> present;
  return;
 }

 ($windowo, my $vbox) = create_window($window, $d->get('OCR'), FALSE);

# OCR engine selection
 my $hboxe = Gtk2::HBox -> new;
 $vbox -> pack_start($hboxe, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('OCR Engine'));
 $hboxe -> pack_start ($label, FALSE, FALSE, 0);
 my $combobe = combobox_from_array($SETTING{'ocr engine'}, @ocr_engine);
 $hboxe -> pack_end ($combobe, FALSE, FALSE, 0);
 my ($comboboxl, @tesslang);
 if ($dependencies{tesseract}) {
  (my $hboxl, $comboboxl, @tesslang) = add_ocr_languages($vbox);
  $hboxl->hide_all
   if ($ocr_engine[$combobe -> get_active]->[0] ne 'tesseract');
  $combobe -> signal_connect (changed => sub {
   if ($ocr_engine[$combobe -> get_active]->[0] eq 'tesseract') {
    $hboxl->show_all;
   }
   else {
    $hboxl->hide_all;
   }
  });
 }

# Frame for page range
 my ($buttona, $buttonc, $buttons) = add_page_range($vbox);

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

# Start button
 my $obutton = Gtk2::Button -> new($d->get('Start OCR'));
 $hbox -> pack_start( $obutton, TRUE, TRUE, 0 );
 $obutton -> signal_connect( clicked => sub {

  $SETTING{'ocr engine'} = $ocr_engine[$combobe -> get_active]->[0];
  $SETTING{'ocr language'} = $tesslang[$comboboxl -> get_active]->[0]
   if ($SETTING{'ocr engine'} eq 'tesseract');

# fill $pagelist with filenames depending on which radiobutton is active
  my @pagelist = get_page_index();
  if (! @pagelist) {
   show_message_dialog($windowo, 'error', 'close', $d->get('No page selected'));
   return;
  }
  ocr_page(@pagelist);
 } );

# Close button
 my $cbutton = Gtk2::Button -> new_from_stock('gtk-close');
 $hbox -> pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton -> signal_connect( clicked => sub { $windowo -> hide; } );

 $windowo -> show_all;
}


# 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;
 $SETTING{'ocr panel'} = $vpaned -> get_position;

# Save options setting
 $SETTING{'enable options'} =
  $uimanager->get_widget('/MenuBar/Edit/Options') -> get_active;

# Save options setting
 $SETTING{'restore window'} =
  $uimanager->get_widget('/MenuBar/Edit/RestoreWindow') -> get_active;

# delete $SETTING{'RMB'};

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

 kill_subs();
}


# View POD

sub view_pod {

 if (defined $windowh) {
  $windowh -> present;
  return;
 }

 eval {require Gtk2::Ex::PodViewer};
 if ($@) {
  show_message_dialog($window, 'error', 'close',
   sprintf($d->get("The help viewer requires module Gtk2::Ex::PodViewer\n"
                        ."Alternatively, try: %s %s\n\n"), $program, "--help")
  );
  return;
 }

# 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::Ex::Simple::List->new('icon' => 'pixbuf',
                                         'title' => 'text',
                                         'link' => 'hstring');
 $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);
 $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 TRUE;
 });

 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 ($options, $value) = @_;
 foreach (keys %$options) {
  return $_ if ($options->{$_}{string} eq $value);
 }
 return FALSE;
}


# Get value string from combobox

sub get_value {
 my ($options, $option, $value) = @_;
 foreach (keys %{$options->{$option}{values}}) {
  return $_ if ($options->{$option}{values}{$_} eq $value);
 }
 return FALSE;
}


# Update undo/redo buffers before doing something

sub take_snapshot {

# Deep copy the tied data. Otherwise, very bad things happen.
 @undo_buffer = map { [ @$_ ] } @{ $slist->{data} };
 @undo_selection = $slist -> get_selected_indices;

# Unghost Undo/redo
 $uimanager->get_widget('/MenuBar/Edit/Undo') -> set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Undo') -> set_sensitive(TRUE);
}


# Put things back to last snapshot after updating redo buffer

sub undo {

# Deep copy the tied data. Otherwise, very bad things happen.
 @redo_buffer = map { [ @$_ ] } @{ $slist->{data} };
 @redo_selection = $slist -> get_selected_indices;

# Block slist signals whilst updating
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 @{$slist->{data}} = @undo_buffer;

# Unblock slist signals now finished
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Reselect the pages to display the detail view
 $slist->select(@undo_selection);

# Update menus/buttons
 $uimanager->get_widget('/MenuBar/Edit/Undo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Edit/Redo') -> set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Undo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Redo') -> set_sensitive(TRUE);
}


# Put things back to last snapshot after updating redo buffer

sub unundo {

# Deep copy the tied data. Otherwise, very bad things happen.
 @undo_buffer = map { [ @$_ ] } @{ $slist->{data} };
 @undo_selection = $slist -> get_selected_indices;

# Block slist signals whilst updating
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 @{$slist->{data}} = @redo_buffer;

# Unblock slist signals now finished
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Reselect the pages to display the detail view
 $slist->select(@redo_selection);

# Update menus/buttons
 $uimanager->get_widget('/MenuBar/Edit/Undo') -> set_sensitive(TRUE);
 $uimanager->get_widget('/MenuBar/Edit/Redo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo') -> set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Redo') -> set_sensitive(FALSE);
}


# Initialise IconFactory

sub init_icons {
 return if defined $IconFactory;

 $IconFactory = Gtk2::IconFactory->new();
 $IconFactory->add_default();

 foreach ( @_ ) {
  register_icon($_->[0], $_->[1]);
 }
}


# Add icons

sub register_icon {
 my ($stock_id, $path) = @_;

 return unless defined $IconFactory;

 my $icon;
 eval { $icon = Gtk2::Gdk::Pixbuf->new_from_file($path); };
 if ($@) {
  warn("Unable to load icon at `$path': $@");
 }
 else {
  my $set = Gtk2::IconSet->new_from_pixbuf($icon);
  $IconFactory->add($stock_id, $set);
 }
}


# Process the exit of the child. If you were doing something useful,
# you might keep things like information about what data needs
# to be reloaded when a child process exits.

sub sig_child {
 my $pid = waitpid(-1, &WNOHANG);

 if ($pid == -1) {
  # no child waiting.  Ignore it.
 }
 elsif (WIFEXITED($?)) {
  print "Process $pid exited.\n";
  delete $helperTag{$pid};
 }
 else {
  print "False alarm on $pid.\n";
 }
 $SIG{CHLD} = \&sig_child;                  # install *after* calling waitpid
}

sub start_process {
 my ($process) = @_;

 $reader = FileHandle->new;
 $writer = FileHandle->new;
 socketpair($reader, $writer,  AF_UNIX, SOCK_DGRAM, PF_UNSPEC);

 my $pid = fork();
 if ($pid) {

# We're still in the parent; note pid and watch the streams:
  shutdown($writer, 0);

  warn "Forked PID $pid\n" if ($debug);
  return $pid;
 }
 else {

# We're in the child. Do whatever processes we need to. We *must*
# exit this process with POSIX::_exit(...), because exit() would
# "clean up" open file handles, including our display connection,
# and merely returning from this subroutine in a separate process
# would *really* confuse things.
  shutdown($reader, 1);

# reseed the randomiser to prevent
# "Have exceeded the maximum number of attempts (10) to open temp file/dir"
# errors from File::Temp
  srand($$);

  $process->();
  POSIX::_exit(0);
 }
}


# We should clean up after ourselves so that we don't
# leave dead processes flying around.
sub kill_subs {

# 15 = SIGTERM
 kill 15, $_ foreach (keys %helperTag);
}


__END__

=head1 Name

gscan2pdf - A GUI to produce a multipage PDF from a scan.
gscan2pdf should work on almost any Linux/BSD machine.

=for html <p align="center">
 <img src="https://sourceforge.net/dbimage.php?id=121788" border="1" width="632"
 height="480" alt="Screenshot" /><br/>Screenshot: Main page v0.9.9</p>

=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

Scanning is handled with SANE via scanimage.
PDF conversion is done by PDF::API2.
TIFF export is handled by libtiff (faster and smaller memory footprint for
multipage files).

=head1 Download

gscan2pdf is available on Sourceforge
(L<https://sourceforge.net/project/showfiles.php?group_id=174140&package_id=199621>).

=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>

If you add my key to your list of trusted keys, then you will no longer get
the "not authenticated" warnings:

C<gpg --keyserver www.keyserver.net --recv-keys 4DD7CC93>

C<gpg --export --armor 4DD7CC93 | sudo apt-key add ->

=head2 RPMs

Download the rpm from Sourceforge, and then install it with
C<rpm -i gscan2pdf-version.rpm>

=head2 From source

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

=head2 From the repository

gscan2pdf uses Mercurial for its Revision Control System. You can browse the
tree at L<http://gscan2pdf.sourceforge.net/cgi-bin/hgwebdir.cgi/gscan2pdf/>.
You can also download snapshots as .tar.gz, .zip, and .tar.bz2.

Mercurial users can clone the complete tree with
C<hg clone http://gscan2pdf.sourceforge.net/cgi-bin/hgwebdir.cgi/gscan2pdf>

=head1 Building gscan2pdf from source

Having downloaded the source either from a Sourceforge file release, or from the
Mercurial repository, unpack it if necessary with
C<tar xvfz gscan2pdf-x.x.x.tar.gz
cd gscan2pdf-x.x.x>

C<perl Makefile.PL>, will create the Makefile.
There is a C<make test>, but this is not machine-dependent, and therefore really
just for my benefit to make sure I haven't broken the device-dependent options
parsing routine.

You can install directly from the source with C<make install>, but building the
appropriate package for your distribution should be as straightforward as
C<make debdist> or C<make rpmdist>. However, you will
additionally need the rpm, devscripts, fakeroot, debhelper and gettext packages.

=head1 Dependencies

The list below looks daunting, but all packages are available from any
reasonable up-to-date distribution. If you are using Synaptic, having installed
gscan2pdf, locate the gscan2pdf entry in Synaptic, right-click it and you can
install them under I<Recommends>.

=over

=item Required

=over

=item libgtk2.0-0 (>= 2.4)

The GTK+ graphical user interface library.

=item libglib-perl (>= 1.100-1)

Perl interface to the GLib and GObject libraries

=item libgtk2-ex-simple-list-perl

A simple interface to Gtk2's complex MVC list widget

=item libgtk2-perl (>= 1:1.043-1)

Perl interface to the 2.x series of the Gimp Toolkit library

=item liblocale-gettext-perl (>= 1.05)

Using libc functions for internationalization in Perl

=item libpdf-api2-perl

provides the functions for creating PDF documents in Perl

=item libsane

API library for scanners

=item libtiff-tools

TIFF manipulation and conversion tools

=item Imagemagick

Image manipulation programs

=item perlmagick

A perl interface to the libMagick graphics routines

=item sane-utils

API library for scanners -- utilities.

=back

=item Optional

=over

=item sane

scanner graphical frontends. Only required for the scanadf frontend.

=item libgtk2-ex-podviewer-perl

Perl Gtk2 widget for displaying Plain Old Documentation (POD). Not required if
you don't need the gscan2pdf documentation (which is anyway repeated on the
website).

=item unpaper

post-processing tool for scanned pages. See L<http://unpaper.berlios.de/>.

=item xdg-utils

Desktop integration utilities from freedesktop.org. Required for Email as PDF.
See L<http://portland.freedesktop.org/wiki/>

=item djvulibre-bin

Utilities for the DjVu image format. See L<http://djvu.sourceforge.net/>

=item gocr

A command line OCR. See L<http://jocr.sourceforge.net/>.

=item tesseract

A command line OCR. See L<http://code.google.com/p/tesseract-ocr/>

=back

=back

=head1 Support

There are two mailing lists for gscan2pdf:

=over

=item gscan2pdf-announce

A low-traffic list for announcements, mostly of new releases. You can subscribe
at L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-announce>

=item gscan2pdf-help

General support, questions, etc.. You can subscribe at
L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-help>

=back

=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<gscan2pdf --debug> with any new bug report.

=head1 Translations

gscan2pdf has already been partly translated several languages.
If you would like to contribute to an existing or new translation, please check
out Rosetta: L<https://launchpad.net/products/gscan2pdf/trunk/+pots/gscan2pdf>

=head1 Menus

=head2 File

=head3 New

Clears the page list.

=head3 Import

Imports any format that imagemagick supports. PDFs will have their embedded
images extracted and imported one per page.

=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 image

Saves the current, selected or all pages as a TIFF, PNG, JPEG, PNM or GIF.

=head3 Save DjVu

Saves the current, selected or all pages as a DjVu.
Currently, only bitonal compression is available.
This is best suited to black and white scans and as such, produces better
compression than PDF. See L<http://www.djvuzone.org/> for more details.

=head3 Email as PDF

Attaches the current, selected or all pages as a PDF to a blank email.
This requires xdg-email, which is in the xdg-utils package.
If this is not present, the option is ghosted out.

=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.

=head3 Frontend

gscan2pdf supports two frontends, scanimage and scanadf.
scanadf support was added when it was realised that scanadf works better with
some scanners than scanimage. On Ubuntu Edgy, scanadf is in the sane package,
not, like scanimage, in sane-utils. If scanadf is not present, the option is
obviously ghosted out.

=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

The rotate options require the package imagemagick and, if this is not present,
are ghosted out.

=head3 Rotate 180

=head3 Rotate 90 anticlockwise

=head2 Tools

=head3 unpaper

unpaper (see L<http://unpaper.berlios.de/>) is a utility for cleaning up a scan.

=head3 OCR (Optical Character Recognition)

The gocr or tesseract utilities are used to produce text from an image.

There is an OCR output buffer for each page and is embedded both as an
annotation (pop-up note) and as plain text behind the scanned image in the PDF
produced. This way, Beagle can index (i.e. search) the plain text, and the
contents of the annotations can be viewed in Acrobat Reader.

In DjVu files, the OCR output buffer is embedded in the hidden text layer.
Thus these can also be indexed by Beagle.

There is an interesting review of OCR software at L<http://groundstate.ca/ocr>.
An important conclusion was that 400dpi is necessary for decent results.

=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.

=head2 Why is option xyz ghosted out?

Probably because the package required for that option is not installed.
Email as PDF requires xdg-email (xdg-utils), scanadf and the rotate options
require imagemagick.

=head2 Why doesn't Email to PDF work with Thunderbird?

Because xdg-email doesn't support creating new emails with attachments in
Thunderbird.

=head2 Why can I not scan from the flatbed of my HP scanner?

Generally for HP scanners with an ADF, to scan from the flatbed, you should
set "# Pages" to "1", and possibly "Batch scan" to "No".

=head2 When I update gscan2pdf using the Update Manager in Ubuntu, why is the list of changes never displayed?

As far as I can tell, this is pulled from changelogs.ubuntu.com, and therefore
only the changelogs from official Ubuntu builds are displayed.

=head1 Roadmap

=over

=item v0.9.17

=over

=item Progress bar for scan

=item Correct positioning and size of OCR output behind scan.

=item More numbering control in scan dialog - useful if scanner jams.

=back

=item v1.0.0

=over

=item Scan profiles.

=back

=item When I've worked out how to do it

=over

=item Crop function.

=item Support for non-bitonal compression in djvu.

=item Import DjVu (using decoders from DjVuLibre)

=back

=back

=head1 See Also

Xsane

=head1 Author

Jeffrey Ratcliffe (ra28145 at users dot sf dot net)

=head1 Thanks to

=over

=item all the people who have sent patches, translations, bugs and feedback.

=item the GTK2 project for a most excellent graphics toolkit.

=item the Gtk2-Perl project for their superb Perl bindings for GTK2.

=item Sourceforge for hosting the project.

=back

=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>
 <a href="http://sourceforge.net/donate/index.php?group_id=174140">
 <img src="http://sourceforge.net/images/project-support.jpg" width="88"
 height="32" border="0" alt="Support This Project"></a>

=cut
