#!/usr/bin/perl

# 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
# 0. option button for unpaper on scan
#    check out scanbuttond
#    saving PDF defaults to /tmp?
#    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?
#    default for no-black-filter in unpaper
#    cover case where unpaper or ocr starts running on a page that has in the
#     meantime been deleted
#    handle corrupted images properly
#    move test tif and correct blank PDF with compression=none
#    use -x option (first column 0-100) in gocr to give more detail to progressbar
#    fix encoding problem with OCR output
#    move locales to locale directory for build
#    look at adding make clean to release procedure
#    add run script option
#    check out freeze when gocr does not return properly
#    add ocrad, ocre support
#    for hidden text layer in DjVu:
#     djvused set-txt
#     DjVutoXML
#     http://djvu.org/resources/embedding_transcripts_in_handwritten_pages.php
#    Add status line at bottom with messages like "filename saved"
#    Rewrite in python with python-sane & pytiff
#    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:
#      Importing (e.g. 12 page multi.tif)
#      Rotating
#      Saving tiff
#    add information to pdf save dialog whilst saving
#    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"
#    Work out a good place to log error messages
#    Add "translate this application" to help menu like gedit, opening launchpad in the default browswer.
#    sort out "dpkg-source: warning: source directory `./gscan2pdf' is not <sourcepackage>-<upstreamversion> `gscan2pdf-0.9.0'" error
#    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 and imports.
# 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
#    Check a translation with LANGUAGE=de gscan2pdf --debug
#    Make sure that changelog reflects History and hg log from bin/gscan2pdf
# 2. make rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf-0....
# 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 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

# 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.13';

# 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/xx_XX/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;
 }
}

if (check_utils()) {
 printf($d->get("%s requires the libtiff library.\nPlease install it.\n"),
                                                                      $program);
 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,
 'Page range'      => 'all',
 'l'               => 0,
 't'               => 0,
 'layout'          => 'single',
);

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

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'/>
  </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
$window -> set_default_size ($SETTING{'window_width'}, $SETTING{'window_height'});
if (defined($SETTING{'window_x'}) and defined($SETTING{'window_y'})) {
 $window -> move ($SETTING{'window_x'}, $SETTING{'window_y'});
}
$window -> maximize if ($SETTING{'window_maximize'});

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

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

# Create the menu bar
my ($menubar, $toolbar) = create_menu_bar( $window );
$main_vbox -> pack_start( $menubar, FALSE, TRUE, 0 );
$main_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;
$main_vbox -> pack_start($hpaned, TRUE, TRUE, 0);

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

# Scrolled window for thumbnails
my $scwin_thumbs = Gtk2::ScrolledWindow -> new;
$hpaned -> pack1($scwin_thumbs, TRUE, TRUE);
$scwin_thumbs -> set_policy('automatic', 'automatic');
$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;
 my $rectangle = $scroll->allocation;
 $tree->set_drag_dest_row($path, $how);

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

 if ($y > $value + $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 < $value + $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'),
                                   },
                                 },
          );

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 ],
 ] );

# 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} =
  system("which convert >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{scanadf} =
  system("which scanadf >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{xdg} =
  system("which xdg-email >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{gocr} =
  system("which gocr >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{tesseract} =
  system("which tesseract >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{djvu} =
  system("which cjb2 >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 $dependencies{unpaper} =
  system("which unpaper >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
 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});
 }

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

# 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);
   }
   $uimanager->get_widget('/MenuBar/File/Save PDF') -> set_sensitive(TRUE);
   $uimanager->get_widget('/MenuBar/File/Save image') -> set_sensitive(TRUE);
   $uimanager->get_widget('/ToolBar/Save PDF') -> set_sensitive(TRUE);
   $uimanager->get_widget('/ToolBar/Save image') -> set_sensitive(TRUE);
   $uimanager->get_widget('/Thumb_Popup/Save PDF') -> 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;
   }
   $uimanager->get_widget('/MenuBar/File/Save PDF') -> set_sensitive(FALSE);
   $uimanager->get_widget('/MenuBar/File/Save image') -> set_sensitive(FALSE);
   $uimanager->get_widget('/ToolBar/Save PDF') -> set_sensitive(FALSE);
   $uimanager->get_widget('/ToolBar/Save image') -> set_sensitive(FALSE);
   $uimanager->get_widget('/Thumb_Popup/Save PDF') -> 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 (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 {
 require Image::Magick;

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

  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);
   undef $image;

   if (! defined $format) {
    show_message_dialog(
     $window, 'error', 'close', $d->get('Not a recognised file')
    );
   }
   elsif ($format eq 'Portable Document Format') {

# Extract images from PDF
    system ("pdfimages \"$filename\" x");

# Import each image
    my @images = <x-???.???>;
    import_scan($_, undef, 'Portable anymap', undef, TRUE) foreach (@images);
   }
   elsif ($format eq 'Tagged Image File Format') {

# Split the tiff into its pages and import them individually
    system("tiffsplit \"$filename\"");
    my @pages = <x???.tif>;
    import_scan($_, undef, $format, undef, TRUE) foreach (@pages);
   }
   elsif ($format =~ /(Portable anymap)|(Portable Network Graphics)|(Joint Photographic Experts Group JFIF format)|(CompuServe graphics interchange format)/) {
    import_scan($filename, undef, $format);
   }
   else {
    my $tiff = convert_to_tiff($filename);
    import_scan($tiff, undef, 'Tagged Image File Format', undef, TRUE);
   }

  }

  update_uimanager();
 }

# cd back to tempdir
 chdir $dir;

 $file_chooser -> destroy;
}


# 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 $framem = Gtk2::Frame -> new($d->get('Metadata'));
 $vbox -> pack_start ($framem, TRUE, TRUE, 0);
 my $vboxm = Gtk2::VBox -> new;
 $vboxm -> set_border_width($border_width);
 $framem -> add ($vboxm);

# Date/time
 my $hboxe = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxe, TRUE, TRUE, 0);
 my $labele = Gtk2::Label -> new ($d->get('Date'));
 $hboxe -> pack_start ($labele, FALSE, FALSE, 0);
 $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');
 my $label = Gtk2::Label -> new ($d->get('Saving PDF')."...");
 $dialog -> vbox -> add($label);

# Set up ProgressBar
 my $pbar = Gtk2::ProgressBar->new;
 $pbar -> set_pulse_step(.1);
 $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();

# Create PDF with PDF::API2
  my $pdf = PDF::API2->new(-file => $filename);
  $pdf->info(get_PDF_options());

  foreach (@pagelist) {
   my $page = $pdf->page;

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

# 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{'pdf compression'} eq 'jpg') {
    (undef, $filename) = tempfile(DIR => $dir, SUFFIX => '.'.$SETTING{'pdf compression'});
    $image->Set(quality => $SETTING{quality})
     if $SETTING{'pdf compression'} eq 'jpg';
    $x = $image->Write($filename);
    warn "$x" if "$x";
    $format = $1 if ($filename =~ /\.(\w*)$/);
   }

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

   $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') {
    $imgobj = $pdf->image_png($filename);
   }
   elsif ($format eq 'jpg') {
    $imgobj = $pdf->image_jpeg($filename);
   }
   elsif ($format eq 'pnm') {
    $imgobj = $pdf->image_pnm($filename);
   }
   elsif ($format eq 'gif') {
    $imgobj = $pdf->image_gif($filename);
   }
   elsif ($format eq 'tif') {
    $imgobj = $pdf->image_tiff($filename);
   }
   else {
    warn "Error embedding file $filename in $format format to PDF\n";
   }
   $gfx->image($imgobj,0,0,72/$resolution) if (defined $imgobj);
  }
  $pdf->save;
  $pdf->end();
 });

# Timer will run until callback returns false 
 Glib::Timeout->add (100, sub { if (defined $helperTag{$pid}) {
                                 $pbar->pulse;
                                 return TRUE;
                                }
                                else {
                                 $dialog -> destroy;
                                 return FALSE;
                                } });
}


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

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

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

 return ($combobc, $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 ($combobc, $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{'pdf compression'} = $compression[$combobc->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[$combobc->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;

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

  my @pagelist = split / /, $pagelist;
  foreach (@pagelist) {

   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
  my $output = `tiffcp -c $compression $pagelist "$filename" 2>&1`;
  if ($output eq '') {
   $windowi -> hide if defined $windowi;
  }
  else {
   unlink $filename;
   show_message_dialog($windowi, 'error', 'close', $output);
  }
 }

 $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 $combobc = Gtk2::ComboBox->new_text;

# Fill compression ComboBox
  $combobc->append_text ($d->get('Bitonal'));
  $hboxc -> pack_end ($combobc, 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;
  }

# 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();
   
  if ($n > 1) {
   my @pagelist = split / /, $pagelist;
   foreach (@pagelist) {

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

# Create the djvu
    system ("cjb2 $_ $djvu");

    $_ = $djvu;
   }
   $pagelist = "@pagelist";
   system ("djvm -c $filename $pagelist");
  }
  else {
   system ("cjb2 $pagelist $filename");
  }

  $windowv -> hide;
 }

 $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 ($combobc, $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 {

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

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

# dig out the compression
  $SETTING{'pdf compression'} = $compression[$combobc->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[$combobc->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";

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

# 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 = 0;
 if (defined($SETTING{'device'})) {
  for (my $i = 0; $i <= $#device; $i++) {
   $o = $i if ($SETTING{'device'} eq $device[$i]);
  }
 }
   
# 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);

# Checkbox for unpaper
 my $ubutton = Gtk2::CheckButton -> new($d->get('unpaper scanned pages'));
 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);
 }
 $vbox -> pack_start($ubutton, TRUE, TRUE, 0);

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

# 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};
     }
    }
   }
  }
  if ($debug) {
   use Data::Dumper;
   print Dumper(\%options);
  }

# 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;
  $SETTING{'ocr engine'} = $ocr_engine[$comboboxe -> get_active]->[0]
   if ($SETTING{'OCR on scan'});
  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;
  });
  $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 %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',
 );

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

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

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

# 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);
 
 if ($unpaper) {
  unpaper_page($ocr, undef, @page);
 }
 else {
  ocr_page(@page) if ($ocr);
 }

 update_uimanager();

 return TRUE;
}


# 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
Sascha Hunold
Chris Mayo
David Hampton
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;
Petr Jelnek
Hugo Pereira
Jen Fraggle
Alberto Boiti
Alexandre Prokoudine
Piotr Strebski
Maddy67
mecedesjorge
Christoph Langner
cwchien
booxter
Nicolas Stransky
Daniel Nylander
Tikkel
Mathieu Goeminne
Nicolas Velin
Simon Leblanc
Eric Spierings
Jacob Nielsen
EOS
 $about->set_translator_credits ($translators);
 $about->set_artists ('lodp');
 $about->run;
 $about->destroy;
}


# Check that tiffcp exists

sub check_utils {
 system("which tiffcp >/dev/null 2>/dev/null");
 return $? if $? != 0;
 return 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 = do { local( @ARGV, $/ ) = $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'),
                                      'Letter'  => $d->get('Letter'),
                                      '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 $spin_button = Gtk2::SpinButton -> new_with_range($1, $2, 1);
    $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 = do { local( @ARGV, $/ ) = $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;
 while ($output =~ /--([\w\-]*)[ \[=\(]*([\w\-\|\.]*)[\)\]]* \[(\w*)\] *\n([\S\s]*)/) {
  my $option = $1;
  my $values = $2;
  my $default = $3;

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

# Set default from config
  $default = $SETTING{$option}
   if ($default !~ /inactive/ and defined($SETTING{$option})

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

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

# Remove everything on the description line and above.
   $output = $2;
  }

  $hash{$option}{values} = $values;
  $hash{$option}{default} = $default;
  $hash{$option}{tip} = $tip;
 }
 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 ($options =~ /\|/) {
  my $value = substr($options, 0, index($options, "|"));
  $options = substr($options, index($options, "|")+1, length($options));
  $value = substr($value, 1, length($value)) while ($value =~ /^ /);
  $value = substr($value, 3, length($value)) while ($value =~ /^\[=\(/);
  push @array, $value;
 }

# Do it all again for the last one
 $options = substr($options, 0, length($options)-2) while ($options =~ /\)\]/);
 push @array, $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) = @_;

 require Image::Magick;

# 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";
  $x = $image->Rotate($degrees);
  warn "$x" if "$x";
  $x = $image->Write(filename => $slist -> {data}[$page[$i]][2]);
  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 ] 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) = @{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 $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
   }
  });
 }
}


# Run unpaper to clean up scan.

sub unpaper {

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

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

 my %option = (
  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).'),
  },
  'no-deskew' => {
   string => $d->get('No deskew'),
   tooltip => $d->get('Disable deskewing.'),
  },
  'no-mask-scan' => {
   string => $d->get('No mask scan'),
   tooltip => $d->get('Disable mask detection.'),
  },
  'no-blackfilter' => {
   string => $d->get('No black filter'),
   tooltip => $d->get('Disable black area scan.'),
  },
  'no-grayfilter' => {
   string => $d->get('No gray filter'),
   tooltip => $d->get('Disable gray area scan.'),
  },
  'no-noisefilter' => {
   string => $d->get('No noise filter'),
   tooltip => $d->get('Disable noise filter.'),
  },
  'no-blurfilter' => {
   string => $d->get('No blur filter'),
   tooltip => $d->get('Disable blur filter.'),
  },
  'no-border-scan' => {
   string => $d->get('No border scan'),
   tooltip => $d->get('Disable border scanning.'),
  },
  'no-border-align' => {
   string => $d->get('No border align'),
   tooltip => $d->get('Disable aligning of the area detected by border scanning.'),
  },
  'deskew-scan-direction' => {
   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."),
   left => {
    string => $d->get('Left'),
    tooltip => $d->get("Use 'left' for scanning from the left edge."),
   },
   top => {
    string => $d->get('Top'),
    tooltip => $d->get("Use 'top' for scanning from the top edge."),
   },
   right => {
    string => $d->get('Right'),
    tooltip => $d->get("Use 'right' for scanning from the right edge."),
   },
   bottom => {
    string => $d->get('Bottom'),
    tooltip => $d->get("Use 'bottom' for scanning from the bottom."),
   },
  },
  'border-align' => {
   string => $d->get('Align to edge'),
   tooltip => $d->get('Edge to which to align the page.'),
   left => {
    string => $d->get('Left'),
    tooltip => $d->get("Use 'left' to align to the left edge."),
   },
   top => {
    string => $d->get('Top'),
    tooltip => $d->get("Use 'top' to align to the top edge."),
   },
   right => {
    string => $d->get('Right'),
    tooltip => $d->get("Use 'right' to align to the right edge."),
   },
   bottom => {
    string => $d->get('Bottom'),
    tooltip => $d->get("Use 'bottom' to align to the bottom."),
   },
  },
  'border-margin' => {
   string => $d->get('Margin'),
   tooltip => $d->get('Margin for aligned edges.'),
  },
  'white-threshold' => {
   string => $d->get('White threshold'),
   tooltip => $d->get('Brightness ratio above which a pixel is considered white.'),
  },
  'black-threshold' => {
   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.'),
  },
 );

# Layout ComboBox
 my $hboxl = Gtk2::HBox -> new;
 $vbox -> pack_start($hboxl, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Layout'));
 $hboxl -> pack_start ($label, FALSE, FALSE, 0);
 my $ev = Gtk2::EventBox->new;
 my $combobl = Gtk2::ComboBox->new_text;
 $ev->add($combobl);

# Fill layout combobox updating the tooltips when changed
 my @layout = qw/single double/;
 foreach (@layout) {
  $combobl -> append_text ($option{$_}{string});
 }
 $combobl -> signal_connect (changed => sub {
  $tooltips -> set_tip ($ev, $option{$layout[$combobl->get_active]}{tooltip});
 });

# Set default tooltip
 $tooltips -> set_tip ($ev, $option{single}{tooltip});
 $hboxl -> pack_end ($ev, FALSE, FALSE, 0);
 $combobl -> set_active (0);

# 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 = Gtk2::CheckButton -> new($option{'no-deskew'}{string});
 $tooltips -> set_tip ($dsbutton, $option{'no-deskew'}{tooltip});
 $vbox1 -> pack_start ($dsbutton, TRUE, TRUE, 0);

# Frame for Deskew Scan Direction
 my $dframe = Gtk2::Frame -> new($option{'deskew-scan-direction'}{string});
 $vbox1 -> pack_start ($dframe, TRUE, TRUE, 0);
 my $vboxd = Gtk2::VBox -> new;
 $vboxd -> set_border_width($border_width);
 $dframe -> add ($vboxd);
 $dsbutton -> signal_connect (toggled => sub {
  if ($dsbutton -> get_active) {
   $dframe->set_sensitive(FALSE);
  }
  else {
   $dframe->set_sensitive(TRUE);
  }
 });

 foreach (qw/left top right bottom/) {
  my $button = Gtk2::CheckButton -> new($option{'deskew-scan-direction'}{$_}{string});
  $tooltips -> set_tip ($button, $option{'deskew-scan-direction'}{tooltip}
                            ." ".$option{'deskew-scan-direction'}{$_}{tooltip});
  $button->set_active(TRUE) if (/left/ or /right/);

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

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

 my $bsbutton = Gtk2::CheckButton -> new($option{'no-border-scan'}{string});
 $tooltips -> set_tip ($bsbutton, $option{'no-border-scan'}{tooltip});
 $vbox2 -> pack_start ($bsbutton, TRUE, TRUE, 0);

 my $babutton = Gtk2::CheckButton -> new($option{'no-border-align'}{string});
 $tooltips -> set_tip ($babutton, $option{'no-border-align'}{tooltip});
 $vbox2 -> pack_start ($babutton, TRUE, TRUE, 0);

# Frame for Align Border
 my $bframe = Gtk2::Frame -> new($option{'border-align'}{string});
 $vbox2 -> pack_start ($bframe, TRUE, TRUE, 0);
 my $vboxb = Gtk2::VBox -> new;
 $vboxb -> set_border_width($border_width);
 $bframe -> add ($vboxb);
 $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 $hboxd = Gtk2::HBox -> new;
 $hboxd->set_sensitive(FALSE);
 my $spin_buttonv = Gtk2::SpinButton -> new_with_range(0, 10, 1);
 my $spin_buttonh = Gtk2::SpinButton -> new_with_range(0, 10, 1);
 $tooltips -> set_tip ($spin_buttonv, $d->get('Vertical margin'));
 $tooltips -> set_tip ($spin_buttonh, $d->get('Horizontal margin'));

 foreach (qw/left top right bottom/) {
  my $button = Gtk2::CheckButton -> new($option{'border-align'}{$_}{string});
  $tooltips -> set_tip ($button, $option{'border-align'}{tooltip}
                            ." ".$option{'border-align'}{$_}{tooltip});
  $vboxb -> pack_start($button, TRUE, TRUE, 0);

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

 my $labelb = Gtk2::Label -> new ($option{'border-margin'}{string});
 $hboxd -> pack_start ($labelb, FALSE, FALSE, 0);

# border margin v entry
 my $labelv = Gtk2::Label -> new ('v');
 $hboxd -> pack_start ($labelv, FALSE, FALSE, 0);
 $hboxd -> pack_start ($spin_buttonv, FALSE, FALSE, 0);

# border margin v entry
 my $labelh = Gtk2::Label -> new ('h');
 $hboxd -> pack_start ($labelh, FALSE, FALSE, 0);
 $hboxd -> pack_start ($spin_buttonh, FALSE, FALSE, 0);
 $vboxb -> pack_start($hboxd, TRUE, TRUE, 0);

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

# White threshold
 my $hboxwt = Gtk2::HBox -> new;
 my $labelwt = Gtk2::Label -> new ($option{'white-threshold'}{string});
 $hboxwt -> pack_start ($labelwt, FALSE, FALSE, 0);
 my $spinbuttonwt = Gtk2::SpinButton -> new_with_range(0, 1, .01);
 $spinbuttonwt->set_value(0.9);
 $tooltips -> set_tip ($spinbuttonwt, $option{'white-threshold'}{tooltip});
 $hboxwt -> pack_end ($spinbuttonwt, FALSE, FALSE, 0);
 $vbox3 -> pack_start ($hboxwt, TRUE, TRUE, 0);

# Black threshold
 my $hboxbt = Gtk2::HBox -> new;
 my $labelbt = Gtk2::Label -> new ($option{'black-threshold'}{string});
 $hboxbt -> pack_start ($labelbt, FALSE, FALSE, 0);
 my $spinbuttonbt = Gtk2::SpinButton -> new_with_range(0, 1, .01);
 $spinbuttonbt->set_value(0.33);
 $tooltips -> set_tip ($spinbuttonbt, $option{'black-threshold'}{tooltip});
 $hboxbt -> pack_end ($spinbuttonbt, FALSE, FALSE, 0);
 $vbox3 -> pack_start ($hboxbt, TRUE, TRUE, 0);

 my $msbutton = Gtk2::CheckButton -> new($option{'no-mask-scan'}{string});
 $tooltips -> set_tip ($msbutton, $option{'no-mask-scan'}{tooltip});
 $vbox3 -> pack_start ($msbutton, TRUE, TRUE, 0);

 my $bfbutton = Gtk2::CheckButton -> new($option{'no-blackfilter'}{string});
 $tooltips -> set_tip ($bfbutton, $option{'no-blackfilter'}{tooltip});
 $vbox3 -> pack_start ($bfbutton, TRUE, TRUE, 0);

 my $gfbutton = Gtk2::CheckButton -> new($option{'no-grayfilter'}{string});
 $tooltips -> set_tip ($gfbutton, $option{'no-blackfilter'}{tooltip});
 $vbox3 -> pack_start ($gfbutton, TRUE, TRUE, 0);

 my $nfbutton = Gtk2::CheckButton -> new($option{'no-noisefilter'}{string});
 $tooltips -> set_tip ($nfbutton, $option{'no-noisefilter'}{tooltip});
 $vbox3 -> pack_start ($nfbutton, TRUE, TRUE, 0);

 my $blbutton = Gtk2::CheckButton -> new($option{'no-blurfilter'}{string});
 $tooltips -> set_tip ($blbutton, $option{'no-blurfilter'}{tooltip});
 $vbox3 -> pack_start ($blbutton, TRUE, TRUE, 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);

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

  $SETTING{layout} = get_key(\%option, $combobl -> get_active_text);

  my $deskew = '';
  if ($dsbutton -> get_active) {
   $deskew = '--no-deskew';
  }
  else {
   foreach ($vboxd -> get_children) {
    if ($_ -> get_active) {
     $deskew .= "," if ($deskew ne '');
     $deskew .= get_key($option{'deskew-scan-direction'}, $_ -> get_label);
    }
   }
   $deskew = "--deskew-scan-direction $deskew";
  }

  my $border_align = '';
  if ($bsbutton -> get_active) {
   $border_align = '--no-border-scan';
  }
  elsif ($babutton -> get_active) {
   $border_align = '--no-border-align';
  }
  else {
   foreach ($vboxb -> get_children) {
    if ($_ -> isa('Gtk2::CheckButton') and $_ -> get_active) {
     $border_align .= "," if ($border_align ne '');
     $border_align .= get_key($option{'border-align'}, $_ -> get_label);
    }
   }
   $border_align = "--border-align $border_align"
    ." --border-margin ".$spin_buttonv->get_value.",".$spin_buttonh->get_value
    if ($border_align ne '');
  }

  my $white = "--white-threshold ".$spinbuttonwt->get_value;
  my $black = "--black-threshold ".$spinbuttonbt->get_value;
  
  my $maskscan = '';
  $maskscan = '--no-mask-scan' if ($msbutton -> get_active);

  my $blackfilter = '';
  $blackfilter = '--no-blackfilter' if ($bfbutton -> get_active);

  my $grayfilter = '';
  $grayfilter = '--no-grayfilter' if ($gfbutton -> get_active);

  my $noisefilter = '';
  $noisefilter = '--no-noisefilter' if ($nfbutton -> get_active);

  my $blurfilter = '';
  $blurfilter = '--no-blurfilter' if ($blbutton -> get_active);

  my @pagelist = get_page_index();

  my $options = "--layout $SETTING{layout} $deskew $border_align "
   ."$maskscan $white $black $blackfilter $grayfilter $noisefilter $blurfilter";
  
  unpaper_page(undef, $options, @pagelist);
 } );

# 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;
      if ($slist -> {data}[$pagenum][2] !~ /\.tif$/) {
       my $file = $slist -> {data}[$pagenum][2];

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

       $cmd = "convert $file $tif; tesseract $tif $txt; rm $tif";
      }
      else {
       $tif = $slist -> {data}[$pagenum][2];
       $cmd .= "tesseract $tif $txt;";
      }
     }
     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] .= do { local( @ARGV, $/ ) = "$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
   }
  });
 }
}


# 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 $_->[0] eq $default);
  ++$i;
 }
 $o = 1 if (! defined $o);
 $combobox -> set_active ($o);
 
 return $combobox;
}


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

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

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

# 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 my $key ( keys %$options ) {
  return $key if ($key ne 'tooltip' and $key ne 'string'
                                       and $options->{$key}{string} eq $value);
 }
 return FALSE;
}


# Get value string from combobox

sub get_value {
 my ($options, $option, $value) = @_;
 foreach my $key ( keys %{$options->{$option}{values}} ) {
  return $key if ($options->{$option}{values}{$key} 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 ) = @_;

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

# We're still in the parent; note pid

  $helperTag{$pid} = $pid;
  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.
  $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>).

Having downloaded it, C<perl Makefile.PL>, will create the Makefile, C<make>
will build the application and C<make install> will install it for you. Note
there is no C<make test> step. To build the RPM and .deb packages, you will
additionally need the rpm, devscripts, fakeroot, debhelper and gettext packages.

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

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.

=head1 Roadmap

=over

=item v0.9.14

=over

=item Option button for unpaper on scan.

=item More information on save PDF progress bar

=back

=item v1.0.0

=over

=item Progress bars for import, scan & save as TIFF/DjVu

=item Downsample option for PDF export.

=item Scan profiles.

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

=back

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

=over

=item Crop function.

=item Use the "hidden text" layer in DjVu documents.

=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
