#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process
# Copyright (C) 2006 -- 2008  Jeffrey Ratcliffe <Jeffrey.Ratcliffe@gmail.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the version 3 GNU General Public License as
# published by the Free Software Foundation.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# To do
#    add Imagemagick/unsharp mask option
#    add preferences option to change temp dir.
#    Remove instances of 'if (defined($SETTING' and put them in default
#    add units retrieval in Gscan2pdf.pm and put them on the scan dialog
#    rehash the Makefile.PL as per http://www.koders.com/perl/fidEAA93E6AE47B78C81E8991FAD2D79381F23280C2.aspx
#    why doesn't cancel kill the process when running Tess?
#    add option for number of background processes
#    add possibility to have multiple scans from a flatbed with delay between
#    if downgrading, don't read remaining options.
#    note options like source before rescan due to mode change
#    cache options except between versions of SANE and gscan2pdf
#    note sane-backend and wipe scan settings if it changes
#    reenable multiple scans from flatbed + delay and warning
#    add rotate option before OCR/unpaper on scan dialog
#    query downsizing problems with the imagemagick team.
#    crop - 1. sort out redrawing after refresh
#           2. zooming
#           3. not allowing the box over the edge of the page
#    look at using potrace to convert images to vectors
#    add option to overwrite textbuffer with OCR
#    add an option selecting the number of processes to run simultaneously
#    add a larger border inside notebook in scan dialog
#    add (user-definable) metadata fields to DjVu save
#     (e.g. http://www.kdedevelopers.org/node/2653,
#      http://djvu.sourceforge.net/doc/man/djvused.html)
#     test if they are indexed by Beagle and file a bug if not
#    if anybody else complains about spending a long time "Closing PDF",
#     see if $pdf->finishobjects does any good.
#    move paper sizes into TreeModel
#    Translate documentation (po4a?)
#    try a document with 250 pages
#    cope with or prevent pages being deleted during scan/unpaper/ocr
#    write hg2cl if necessary
#    simplify code - only keep variables that are needed
#    - rename those like $hboxu to $hbox
#    get it to work on the TV tuner
#    check out scanbuttond
#    problem with wait after cancelling scan?
#    cover case where unpaper or ocr starts running on a page that has in the
#     meantime been deleted
#    use -x option (first column 0-100) in gocr to give more detail to progressbar
#    add run script option
#    check out freeze when gocr does not return properly
#    add ocrad, ocre support
#    Add status line at bottom with messages like "filename saved"
#    Add KDE, Xfce menus
#    Add hidden flag whether a page has been saved
#    Fix blocking while rotating
#    Use  $tree_view->window->set_cursor( Gtk2::Gdk::Cursor->new('watch') );
#      and    $tree_view->window->set_cursor (undef); when working.
#    Option to throw up PDF viewer with newly created PDF file
#    Right click save to PDF or TIFF should default to page range "selected"
#    Add "translate this application" to help menu like gedit, opening launchpad in the default browswer.
#    Switch viewing widget as soon as packages are available for:
#     http://giv.sourceforge.net/gtk-image-viewer/
#     or http://trac.bjourne.webfactional.com/ (GtkImageView)
#    As soon as I have cups-config --version >= 1.2.2, move print routines to
#     Net::Cups
#    Simplify all save menu items into one.
#     Investigate using Gtk2::FileChooser instead of Gtk2::FileChooserDialog
#     and putting save options on same dialog.
#
# 1. 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
#       and ../histogramplot.pl)
#
# Release procedure:
# 0. Test scan in lineart, greyscale and colour.
# 1. New screendump required? Print screen creates screenshot.png in Desktop.
#    Make sure that file list in .spec and hg manifest reflects MANIFEST
#    Download new translations
#    Update translators in credits
#    Check a translation with LANGUAGE=de bin/gscan2pdf --debug
#    Make appropriate updates to debian/changelog
# 2. perl Makefile.PL
#    make rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf-x.x.x_all.deb
# 3. hg status
#    hg tag vx-x-x
#    hg push ssh://ra28145@shell.sf.net//home/groups/g/gs/gscan2pdf/hg/gscan2pdf
# 4. make remote-dist remote-html 
# 5. Upload to sourceforge and release files
# 6. Freshmeat
# 7. Launchpad, upload .pot if necessary
# 8. http://www.gtkfiles.org/app.php/gscan2pdf
# 9. Ubuntu forum
#    gscan2pdf-announce
#    ftp-master (check contents with dpkg-deb --contents)

use warnings;
use strict;
use Gscan2pdf;
use Gtk2 -init;
use Gtk2::Ex::Simple::List;
use Gtk2::Gdk::Keysyms;
use Cwd;                             # To obtain current working directory
use File::Basename;                  # Split filename into dir, file, ext
use File::Copy;
use File::Temp qw(tempfile tempdir); # To create temporary files
use Glib qw(TRUE FALSE);             # To get TRUE and FALSE
use Socket;
use FileHandle;
use Image::Magick;
use Config::General;

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

my $tolerance = 1;

# Window parameters
my $border_width = 6;

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

# Set up domain for gettext (internationalisation)
# Expects /usr/share/locale/LANGUAGE/LC_MESSAGES/$program.mo
# or whatever is set by $d->dir([NEWDIR]);
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") == 0 or warn $d->get("Error displaying help\n");
  exit;
 }
 elsif ($ARGV[0] eq "--locale") {
  shift @ARGV;
  if ($ARGV[0] !~ /^\//) {
   $d->dir(getcwd."/".shift @ARGV);
  }
  else {
   $d->dir(shift @ARGV);
  }
 }
 elsif ($ARGV[0] eq "--debug") {
  $debug = TRUE;
  shift @ARGV;
  warn "$program $version\n";
 }
 else {
  warn sprintf($d->get("Unknown option %s.\n"), shift @ARGV); # xgettext hack
  exit 1;
 }
}

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

# Read config file
my $rc = "$ENV{'HOME'}/.$program";
system("touch $rc") if (! -r $rc);
my $conf = Config::General->new(
 -ConfigFile        => $rc,
 -SplitPolicy       => 'equalsign',
 -MergeDuplicateOptions => TRUE,
 -DefaultConfig => {
  'window_width'       => 800,
  'window_height'      => 600,
  'window_maximize'    => TRUE,
  'thumb panel'        => 100,
  'Page range'         => 'all',
  'Page range default' => 'all',
  'l'                  => 0,
  't'                  => 0,
  'layout'             => 'single',
  'downsample'         => FALSE,
  'downsample dpi'     => 150,
  'threshold tool'     => 80,
 },
);
my %SETTING = $conf->getall;
if (! defined $SETTING{Paper}) {
 $SETTING{Paper} = {
  $d->get('A4') => {
   x => 210,
   y => 297,
   l => 0,
   t => 0,
  },
  $d->get('US Letter') => {
   x => 216,
   y => 279,
   l => 0,
   t => 0,
  },
  $d->get('US Legal') => {
   x => 216,
   y => 356,
   l => 0,
   t => 0,
  },
 };
}

# Remove invalid paper sizes
else {
 for my $paper (keys %{$SETTING{Paper}}) {
  if ($paper eq '<>' or $paper eq '</>') {
   delete $SETTING{Paper}{$paper};
  }
  else {
   for (qw(x y t l)) {
    if (! defined $SETTING{Paper}{$paper}{$_}) {
     delete $SETTING{Paper}{$paper};
     last;
    }
   }
  }
 }
}

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.svg" ],
 [ 'pdf',	"$path/pdf.svg" ],
);

# 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, $vboxm, @undo_buffer,
    @redo_buffer, @undo_selection, @redo_selection, %dependencies,
    @ocr_stack, $ocr_timer, %helperTag, @unpaper_stack, $unpaper_timer,
    $scanwatch, @ocr_engine, $bscanall, $bscannum, @clipboard, $windowd,

# Spin buttons on scan dialog
    $spin_buttons, $start, $spin_buttoni, $spin_buttonn,

# Scan button on scan dialog
    $sbutton,

# List of PageRange widgets
    @prlist,

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

my @action_items = (
 # Fields for each action item:
 # [name, stock_id, value, label, accelerator, tooltip, callback]
 
 # File menu
 [ 'File', undef, $d->get('_File') ],
 [ 'New', 'gtk-new', $d->get('_New'), '<control>n', $d->get('Clears all pages'), \&new ],
 [ 'Import', 'gtk-open', $d->get('_Import'), '<control>i', $d->get('Import image file(s)'), \&import ],
 [ 'Scan', 'scanner', $d->get('S_can'), undef, $d->get('Scan document'), \&scan_dialog ],
 [ 'Save PDF', 'pdf', $d->get('_Save PDF'), '<shift><control>s', $d->get('Save as PDF'), \&save_PDF_dialog ],
 [ 'Save image', 'gtk-save', $d->get('Save _Image'), '<control>s', $d->get('Save image'), \&save_image_dialog ],
 [ 'Save DjVu', undef, $d->get('Save _DjVu'), undef, $d->get('Save as DjVu'), \&save_djvu_dialog ],
 [ 'Email as PDF', 'gtk-edit', $d->get('_Email as PDF'), '<control>e', $d->get('Attach as PDF to a new email'), \&email ],
 [ 'Compress', undef, $d->get('_Compress temporary files'), undef, $d->get('Compress temporary files'), \&compress_temp ],
 [ 'Quit', 'gtk-quit', $d->get('_Quit'), '<control>q', $d->get('Quit'), sub { Gtk2 -> main_quit if 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 ],
 [ 'Cut', 'gtk-cut', $d->get('Cu_t'), '<control>x', $d->get('Cut selection'), \&cut_selection ],
 [ 'Copy', 'gtk-copy', $d->get('_Copy'), '<control>c', $d->get('Copy selection'), \&copy_selection ],
 [ 'Paste', 'gtk-paste', $d->get('_Paste'), '<control>v', $d->get('Paste selection'), \&paste_selection ],
 [ '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 {

# Update undo/redo buffers
    take_snapshot();
    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') ],
 [ 'Threshold', undef, $d->get('_Threshold'), undef, $d->get('Change each pixel above this threshold to black'), \&threshold ],
# [ 'Crop', 'GTK_STOCK_LEAVE_FULLSCREEN', $d->get('_Crop'), undef, $d->get('Crop pages'), \&crop ],
 [ 'unpaper', undef, $d->get('_unpaper'), undef, $d->get('Clean up pages with unpaper'), \&unpaper ],
 [ 'OCR', undef, $d->get('_OCR'), undef, $d->get('Optical Character Recognition'), \&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='Compress'/>
   <separator/>
   <menuitem action='Quit'/>
  </menu>
  <menu action='Edit'> 
   <menuitem action='Undo'/>
   <menuitem action='Redo'/>
   <separator/>
   <menuitem action='Cut'/>
   <menuitem action='Copy'/>
   <menuitem action='Paste'/>
   <menuitem action='Delete'/>
   <menuitem action='Renumber'/>
   <menuitem action='Select All'/>
   <separator/>
   <menu action='Frontend'>
    <menuitem action='scanimage'/>
    <menuitem action='scanadf'/>
   </menu>
   <separator/>
   <menuitem action='Options'/>
   <menuitem action='RestoreWindow'/>
  </menu>
  <menu action='View'> 
   <menuitem action='Zoom 100'/>
   <menuitem action='Zoom to fit'/>
   <menuitem action='Zoom in'/>
   <menuitem action='Zoom out'/>
   <separator/>
   <menuitem action='Rotate 90'/>
   <menuitem action='Rotate 180'/>
   <menuitem action='Rotate 270'/>
  </menu>
  <menu action='Tools'> 
   <menuitem action='Threshold'/>
   <separator/>
   <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='Cut'/>
  <toolitem action='Copy'/>
  <toolitem action='Paste'/>
  <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='Cut'/>
  <menuitem action='Copy'/>
  <menuitem action='Paste'/>
  <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='Cut'/>
  <menuitem action='Copy'/>
  <menuitem action='Paste'/>
  <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 { Gtk2 -> main_quit if quit(); } );

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

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

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

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

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

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

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

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

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

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

# 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',
                                       'Saved' => 'hstring');
$slist -> get_selection -> set_mode ('multiple');
$slist -> set_headers_visible(FALSE);
$slist -> set_reorderable( TRUE );

my $target_entry = {
 target => 'Glib::Scalar', # some string representing the drag type
 flags => 'same-widget',   # Gtk2::TargetFlags
 info => 1,                # some app-defined integer identifier
};
$slist->drag_source_set('button1-mask', ['copy', 'move'], $target_entry);
$slist->drag_dest_set(['motion', 'highlight'], ['copy', 'move'], $target_entry);

$slist->signal_connect('drag-data-get' => sub {
 my ($tree, $context, $sel) = @_;
 $sel->set($sel->target, 8, 'data');
});

$slist->signal_connect('drag-data-delete' => sub {
 my ($tree, $context) = @_;
 my $model = $tree->get_model;
 my @data = $tree->get_selection->get_selected_rows;

 for (reverse @data) {
  my $iter = $model->get_iter($_);
  my $info = $model->get($iter, 0);
  $model->remove($iter);
 }

 $tree->get_selection->unselect_all;
});

$slist->signal_connect('drag-data-received' => sub {
 my ($tree, $context, $x, $y, $sel) = @_;
 my ($path, $how) = $tree->get_dest_row_at_pos($x, $y);
 my $model = $tree->get_model;
 my $data = $sel->data or return;
 my $delete = $context->action == 'move';

 my @rows = $tree->get_selection->get_selected_rows or return;
 my @data;
 for (@rows) {
  my $iter = $model->get_iter($_);
  my @info = $model->get($iter);
  my $suffix = $1 if ($info[2] =~ /(\.\w*)$/);
  my (undef, $new) = tempfile(DIR => $dir, SUFFIX => $suffix);
  copy($info[2], $new) or 
   show_message_dialog($window, 'error', 'close', $d->get('Error copying page'));
  $info[2] = $new;
  push @data, [ @info ];
 }

# Block row-changed signal so that the list can be updated before the sort
# takes over.
 $slist -> get_model -> signal_handler_block($slist -> {signalid});

 if ($path) {
  if ($how eq 'after' or $how eq 'into-or-after') {
   splice @{ $slist->{data} }, $path->to_string+1, 0, @data;
  }
  else {
   splice @{ $slist->{data} }, $path->to_string, 0, @data;
  }
 }
 else {
  push @{ $slist->{data} }, @data;
 }
 renumber($slist, 0);
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});

# Update the start spinbutton if necessary
 update_start();

 $context->finish(1, $delete, time);
});

# Callback for dropped signal.
$slist -> signal_connect(drag_drop => sub {
 my ($tree, $context, $x, $y, $when) = @_;
 if (my $targ = $context->targets) {
  $tree->drag_get_data($context, $targ, $when);
  return TRUE;
 }
 return FALSE;
});

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

# Add the marker showing the drop in the tree
 $tree->set_drag_dest_row($path, $how);

# Make move the default
 my @action;
 if ($context->actions == 'copy') {
  @action = ( 'copy' );
 }
 else {
  @action = ( 'move' );
 }
 $context->status(@action, $t);

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

 if ($y > $adj->page_size - $step/2) {
  my $v = $value + $step;
  my $m = $adj->upper - $adj->page_size;
  $adj->set_value($v > $m ? $m : $v);
 }
 elsif ($y < $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);

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

# Update the start spinbutton if necessary
  update_start();
 });

$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 => sub {
 my ($widget, $event) = @_;

# Draw a rubber band if LHB and we have an image
# if ($event->button == 1 and $slist->get_selected_indices) {

# Erase...
#  draw_rubber_band_box ($widget) if ($widget->{draginfo});

#  $widget->{draginfo} = {
#   x1 => $event->x,
#   y1 => $event->y,
#   x2 => $event->x,
#   y2 => $event->y,
#  };

# Draw...
#  draw_rubber_band_box ($widget);

# allow event propagation
# return FALSE;
# }
# else {
  handle_clicks($widget, $event);
# }
});
$eventbox->signal_connect(button_release_event => \&handle_clicks);
#$eventbox->signal_connect_after (realize => sub {
# my ($widget) = @_;
# $widget->{drag_gc} = Gtk2::Gdk::GC->new ($widget->window);
# $widget->{drag_gc}->set_function ('xor');
# $widget->{drag_gc}->set_foreground ($widget->style->white);
#});

#$eventbox->signal_connect_after (unrealize => sub {
# my ($widget) = @_;
# delete $widget->{drag_gc};
#});

#$eventbox->signal_connect (motion_notify_event => sub {
# my ($widget, $event) = @_;

# if ($widget->{draginfo}) {

# Erase...
#  draw_rubber_band_box ($widget);

#  $widget->{draginfo}{x2} = $event->x;
#  $widget->{draginfo}{y2} = $event->y;

# Draw new...
#  draw_rubber_band_box ($widget);
# }

# return FALSE;
#});

# If we need to redraw the rubberband
#$eventbox->signal_connect (expose_event => sub {
# my ($widget, $event) = @_;
#warn "redrawing...\n";

# Let the event propagate
# return FALSE;
#});

$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 eventbox wrapping pixbuf
  my $widthd = $eventbox -> allocation -> width;
  my $heightd = $eventbox -> 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('');
 }

# If one page is selected, PageRange widgets default to either current or all
 if ($#page == 0) {
  $SETTING{'Page range'} = $SETTING{'Page range default'};
 }

# If more than one page is selected, PageRange widgets default to selected
 elsif ($#page > 0) {
  $SETTING{'Page range'} = 'selected';
 }

# No pages is selected, but some exist, PageRange widgets default to all
 elsif ($#{$slist -> {data}} > -1) {
  $SETTING{'Page range'} = 'all';
 }
 foreach (@prlist) {
  $_->set_active($SETTING{'Page range'});
 }
});

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

# 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'),
                                   },
                                 },
            'pagewidth'       => { string => $d->get('Page Width') },
            'pageheight'      => { string => $d->get('Page Height') },
          );

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

update_uimanager();

$window -> show_all;
Gtk2 -> main;



### Subroutines

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 $uimanager->get_widget('/MenuBar/Tools/Threshold') -> set_sensitive(FALSE);
# $uimanager->get_widget('/MenuBar/Tools/Crop') -> 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/Threshold',
                 '/MenuBar/Tools/unpaper',
                 '/MenuBar/Tools/OCR',

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

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

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

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

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

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

# If the scan dialog has already been drawn, update the start page spinbutton
 update_start();
}


# 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 = $eventbox -> allocation -> width;
 my $heightd = $eventbox -> 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}} = ();

# Reset start page in scan dialog
 reset_start();

 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 (keys %{$SETTING{Paper}}) {
  if ($SETTING{Paper}{$_}{x} > 0
      and abs($ratio - $SETTING{Paper}{$_}{y}/$SETTING{Paper}{$_}{x}) < 0.01) {
   $density = (($height > $width) ? $height : $width)/$SETTING{Paper}{$_}{y}*25.4;
  }
 }

# Write the tif
 my (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');
 $image->Write(units => 'PixelsPerInch',
               compression => 'lzw',
               density => $density,
               filename => $tif);
 return $tif;
}


# Throw up file selector and import selected file

sub import {

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

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

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

# cd back to tempdir to import
  chdir $dir;

# Update undo/redo buffers
  take_snapshot();

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

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

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

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

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

  my $pid = start_process(sub {

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

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

# Check if djvu
    my $buffer;
    open FILE, $filename or
     show_message_dialog($window, 'error', 'close',
                          sprintf($d->get("Can't open %s: %s"), $filename, $!));
    binmode FILE;
    read (FILE, $buffer, 8);
    close FILE;
    if ($buffer eq 'AT&TFORM') {
     send($writer, '0 0 0 '.$d->get('Converting to TIFF'), 0);
     my (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');
     my $cmd = "ddjvu -format=tiff \"$filename\" $tif";
     warn "$cmd\n" if ($debug);
     system($cmd);
     $filename = $tif;
    }

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

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

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

# Extract # of pages
     my $info = `pdfinfo \"$filename\"`;
     if ($info =~ /Pages:\s+(\d+)/) {
      my $last = $1;
      my $first = 1;
      if ($last > 1) {
       send($writer, "-2 0 $last", 0);

# Now block until the GUI passes the range back
       my $rin = '';
       my $rout = '';
       vec($rin, $reader->fileno(), 1) = 1;
       if (select($rout=$rin,undef,undef,undef)) {
        my $line;
        recv($reader, $line, 1000, 0);
        ($first, $last) = split ' ', $line;
       }
      }

# Extract images from PDF
      if ($last > 0) {
       send($writer, sprintf("%f %s %i ", $j/($#filename+1), 0, 0)
        .$d->get('Extracting images from PDF'), 0);
       system ("pdfimages -f $first -l $last \"$filename\" x") == 0 or
        send($writer, '-1 0 0'.$d->get('Error extracting images from PDF'), 0);

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

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

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

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

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if (defined($line) and $line ne '') {
     my ($fraction, $filename, $resolution, @text) = split ' ', $line;
     if ($fraction == -1) {
      show_message_dialog($window, 'error', 'close', join(' ', @text));
     }
     if ($fraction == -2) {
      my ($windowq, $vbox) =
       create_window($window, $d->get('Pages to extract'), TRUE);
      my $hbox = Gtk2::HBox -> new;
      $vbox -> pack_start($hbox, TRUE, TRUE, 0);
      my $label = Gtk2::Label -> new ($d->get('First page to extract'));
      $hbox -> pack_start ($label, FALSE, FALSE, 0);
      my $spinbuttonf = Gtk2::SpinButton -> new_with_range(1, $resolution, 1);
      $hbox -> pack_end ($spinbuttonf, FALSE, FALSE, 0);
      $hbox = Gtk2::HBox -> new;
      $vbox -> pack_start($hbox, TRUE, TRUE, 0);
      $label = Gtk2::Label -> new ($d->get('Last page to extract'));
      $hbox -> pack_start ($label, FALSE, FALSE, 0);
      my $spinbuttonl = Gtk2::SpinButton -> new_with_range(1, $resolution, 1);
      $spinbuttonl->set_value($resolution);
      $hbox -> pack_end ($spinbuttonl, FALSE, FALSE, 0);

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

# OK button
      my $obutton = Gtk2::Button -> new_from_stock('gtk-ok');
      $hboxb -> pack_start( $obutton, TRUE, TRUE, 0 );
      $obutton -> signal_connect (clicked => sub {
       my $first = $spinbuttonf->get_value;
       my $last = $spinbuttonl->get_value;
       $windowq -> destroy;
       send($writer, "$first $last", 0);
      });

# Cancel button
      my $cbutton = Gtk2::Button -> new_from_stock('gtk-cancel');
      $hboxb -> pack_start( $cbutton, TRUE, TRUE, 0 );
      $cbutton -> signal_connect (clicked => sub {
       $windowq -> destroy;
       send($writer, '0 0', 0);
      });
      $windowq -> show_all;
     }
     elsif ($fraction > 1) {
      $dialog -> destroy;
      update_uimanager();
      return FALSE;  # uninstall
     }
     else {
      $pbar->set_fraction($fraction);
      $pbar->set_text(join ' ', @text);
      Gtk2->main_iteration while Gtk2->events_pending;
      add_image($filename, undef, $resolution) if ($filename ne '0');
     }
    }
   }

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

# cd back to tempdir
 chdir $dir;
}


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

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

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

 return ($window_new, $vbox);
}


# Add a frame and radio buttons to $vbox,
sub add_page_range {
 my ($vbox) = @_;
 my $frame = Gtk2::Frame->new($d->get('Page Range'));
 $vbox -> add ($frame);

 my $pr = Gscan2pdf::PageRange->new;
 $pr -> set_active($SETTING{'Page range'})
  if (defined $SETTING{'Page range'});
 $pr -> signal_connect (changed => sub {
  $SETTING{'Page range'} = $pr->get_active;
  $SETTING{'Page range default'} = $SETTING{'Page range'}
   if ($SETTING{'Page range'} ne 'selected');
 });
 $frame->add ($pr);
 push @prlist, $pr;
}


# return string of filenames depending on which radiobutton is active

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


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

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


# Add PDF options to $vbox

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

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

# Date/time
 my $hboxe = Gtk2::HBox -> new;
 $vboxm -> pack_start($hboxe, TRUE, TRUE, 0);
 my $labele = Gtk2::Label -> new ($d->get('Date'));
 $hboxe -> pack_start ($labele, FALSE, FALSE, 0);
 $SETTING{'date offset'} = 0 if (! (defined($SETTING{'date offset'})));
 my ($day, $month, $year) =
                    (localtime(time+$SETTING{'date offset'}*24*60*60))[3, 4, 5];
 $year += 1900;
 $month += 1;

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

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

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

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

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

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

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

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

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


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

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


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


# Create the PDF

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

 require PDF::API2;

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

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

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

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

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

 my $pid = start_process(sub {
  my $page = 0;

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

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

   my $page = $pdf->page;

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

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

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

    my $depth = $image->Get('depth');
    if ($SETTING{'downsample'}) {
     $w = $w/$resolution*$SETTING{'downsample dpi'};
     $h = $h/$resolution*$SETTING{'downsample dpi'};
     $resolution = $SETTING{'downsample dpi'};
     $x = $image->Resize(width => $w, height => $h);
     warn "$x" if "$x";
    }
    $x = $image->Set(quality => $SETTING{quality})
     if ($SETTING{'pdf compression'} eq 'jpg');
    warn "$x" if "$x";

    if (($SETTING{'pdf compression'} !~ /(jpg)|(png)/ and $format ne 'tif')
        or ($SETTING{'pdf compression'} =~ /(jpg)|(png)/)
        or $SETTING{'downsample'}) {
     if ($depth > 1) {
      print "Maintaining image depth $depth\n" if $debug;
      $x = $image->Write(filename => $filename, depth => $depth);
     }
     else {
      $x = $image->Write(filename => $filename);
     }
     warn "$x" if "$x";
     $format = $1 if ($filename =~ /\.(\w*)$/);
    }

    if ($SETTING{'pdf compression'} !~ /(jpg)|(png)/) {
     my (undef, $filename2) = tempfile(DIR => $dir, SUFFIX => '.tif');
     my $cmd = "tiffcp -c $SETTING{'pdf compression'} $filename $filename2 2>&1";
     print "$cmd\n" if $debug;
     my $output = `$cmd`;
     my $filename = $filename2;
    }
   }

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

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

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

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

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

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

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

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


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

sub save_PDF {

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

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

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

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

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

 $file_chooser -> destroy;

# cd back to tempdir
 chdir $dir;
}


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

sub add_quality_spinbutton {

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


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

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

# Compression options
 my @compression = (
  [ 'lzw', $d->get('LZW'), $d->get('Compress output with Lempel-Ziv & Welch encoding.') ],
  [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate 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.') ],
  [ 'png',  $d->get('PNG'),  $d->get('Compress output with PNG encoding.') ],
  [ 'jpg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  [ 'none', $d->get('None'), $d->get('Use no compression algorithm on output.') ],
 );

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

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

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


sub save_PDF_dialog {

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

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

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

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

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

# Frame for page range
  add_page_range($vbox);

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

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

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

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

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

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

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


# Display page selector and on save a fileselector.

sub save_image_dialog {

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

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

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

# Frame for page range
 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 = (
  [ 'gif', $d->get('GIF'), $d->get('CompuServe graphics interchange format') ],
  [ 'jpg', $d->get('JPEG'), $d->get('Joint Photographic Experts Group JFIF format') ],
  [ 'png', $d->get('PNG'), $d->get('Portable Network Graphics') ],
  [ 'pnm', $d->get('PNM'), $d->get('Portable anymap') ],
  [ 'ps',  $d->get('PS'), $d->get('Postscript') ],
  [ 'tif', $d->get('TIFF'), $d->get('Tagged Image File 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') {

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

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

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

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

# cd back to tempdir
    chdir $dir;

    $file_chooser -> destroy;
    save_TIFF($filename);
   }
   $file_chooser -> destroy;
  }
  elsif ($SETTING{'image type'} eq 'ps') {

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

# Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog -> new($d->get('PS 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.".ps" if ($filename !~ /\.ps$/i);
    if (file_exists($file_chooser, $filename)) {
     $file_chooser -> destroy;
     return TRUE;
    }

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

# Create the PS
    my (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');
    save_TIFF($tif, $filename);
    unlink $tif;
   }
   $file_chooser -> destroy;
  }
  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 (! -w dirname($filename)) {
  show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
  return TRUE;
 }
 elsif (-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 = get_page_index();

  my @filelist = split / /, $pagelist;
  if ($#filelist == 0) {
   $filename = $filename.".$SETTING{'image type'}"
    if ($filename !~ /\.$SETTING{'image type'}$/i);
   if (! file_exists($file_chooser, $filename)) {
    if (system ("convert $filelist[0] '$filename'")) {
     mark_pages(@pagelist);
    }
    else {
     show_message_dialog($window, 'error', 'close', $d->get('Error saving image'));
    }
   }
  }
  else {
   my $i = 1;
   foreach (@filelist) {
    if (! file_exists($file_chooser, "$filename.$i.$SETTING{'image type'}")) {
     if (system ("convert $_ \"$filename.$i.$SETTING{'image type'}\"")) {
      show_message_dialog($window, 'error', 'close', $d->get('Error saving image'));
     }
     else {
      mark_pages(@pagelist);
     }
    }
    $i++;
   }
  }
 }

 $file_chooser -> destroy;
}


sub save_TIFF {
 my ($filename, $ps) = @_;

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

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

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

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

 my $pid = start_process(sub {

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

  my @pagelist = split / /, $pagelist;
  foreach (@pagelist) {
   ++$page;
   send($writer, ($page-1)/($#pagelist+2)
    .sprintf($d->get("Converting image %i of %i to TIFF"), $page, $#pagelist+1), 0);
    if ($_ !~ /\.tif/) {
    my (undef, $tif) = tempfile(DIR => $dir, SUFFIX => '.tif');

# Convert to tiff
    system ("convert $_ $tif") == 0 or
     send($writer, "2".$d->get('Error writing TIFF'), 0);

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

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

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

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

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

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


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

# Frame for page range
  add_page_range($vbox);

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

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

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

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


sub save_djvu {

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

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

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

  $windowv -> hide;

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

# cd back to tempdir
  chdir $dir;

  $file_chooser -> destroy;

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

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

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

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

  my @pagelist = get_page_index();
  my $pid = start_process(sub {

   my @filelist;
   
   for (my $i = 0; $i < @pagelist; $i++) {
    my $j = $pagelist[$i];
    send($writer, $i/($#pagelist+1)
     .sprintf($d->get("Writing page %i of %i"), $i+1, $#pagelist+1), 0);
    my $filename = $slist -> {data}[$j][2];
    my (undef, $djvu) = tempfile(DIR => $dir, SUFFIX => '.djvu');

# Check the image depth to decide what sort of compression to use
    my $image = Image::Magick->new;
    my $x = $image->Read($filename);
    warn "$x" if "$x";
    my $depth = $image->Get('depth');
    my $compression;

# c44 can only use pnm and jpg
    my $format = $1 if ($filename =~ /\.(\w*)$/);
    if ($depth > 1) {
     $compression = 'c44';
     if ($format !~ /(pnm)|(jpg)/) {
      my (undef, $pnm) = tempfile(DIR => $dir, SUFFIX => '.pnm');
      $x = $image->Write(filename => $pnm);
      warn "$x" if "$x";
      $filename = $pnm;
     }
    }
# cjb2 can only use pnm and tif
    else {
     $compression = 'cjb2';
     if ($format !~ /(pnm)|(tif)/) {
      my (undef, $pnm) = tempfile(DIR => $dir, SUFFIX => '.pnm');
      $x = $image->Write(filename => $pnm);
      warn "$x" if "$x";
      $filename = $pnm;
     }
    }

# Create the djvu
    my $resolution = $slist -> {data}[$j][4];
    my $cmd = "$compression -dpi $resolution $filename $djvu";
    print "$cmd\n" if $debug;
    system ($cmd) == 0 or send($writer, "2".$d->get('Error writing DjVu'), 0);
    $filelist[$i] = $djvu;

# Add OCR to text layer
    if (defined($slist -> {data}[$j][3]) and $slist -> {data}[$j][3] ne '') {

# Get the size
     my $w = $image->Get('width');
     my $h = $image->Get('height');

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

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

# Write djvusedtxtfile
     my $cmd = "djvused '$djvu' -e 'select 1; set-txt djvusedtxtfile' -s";
     print "$cmd\n" if $debug;
     system ($cmd) == 0 or send($writer, "2".$d->get('Error writing DjVu'), 0);
    }
   }
   send($writer, '1'.$d->get('Closing DjVu'), 0);
   my $cmd = "djvm -c '$filename' @filelist";
   print "$cmd\n" if $debug;
   system ($cmd) == 0 or send($writer, "2".$d->get('Error closing DjVu'), 0);
  
   send($writer, "2", 0);
  });

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

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

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


# Display page selector and email.

sub email {

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

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

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

# Frame for page range
 add_page_range($vbox);

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

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

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

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

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

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

# Create the PDF
  create_PDF($pdf);

  system ("xdg-email --attach $pdf 'x\@y'") == 0 or
   show_message_dialog($window, 'error', 'close', $d->get('Error creating email'));

  $windowe -> hide;

 } );

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

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


# Scan

sub scan_dialog {

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

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

# HBox for devices
 my $hboxd = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxd, FALSE, FALSE, 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('Page Options'));

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

# Frame for # pages
 my $framen = Gtk2::Frame -> new($d->get('# Pages'));
 $vbox1 -> 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
 $spin_buttonn = Gtk2::SpinButton -> new_with_range(1, 99, 1);
 $tooltips -> set_tip ($spin_buttonn, $d->get('Set number of pages to scan'));
 $hboxn -> pack_end ($spin_buttonn, 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_buttonn -> set_value($SETTING{'pages to scan'});
  }
 }
 else {
  $bscannum -> set_active(TRUE);
 }

# Toggle to switch between basic and extended modes
 my $checkx = Gtk2::CheckButton -> new($d->get('Extended page numbering'));
 $vbox1 -> pack_start ($checkx, FALSE, FALSE, 0);

# Frame for extended mode
 my $framex = Gtk2::Frame -> new($d->get('Page number'));
 $vbox1 -> pack_start ($framex, FALSE, FALSE, 0);
 my $vboxx = Gtk2::VBox -> new;
 $vboxx -> set_border_width($border_width);
 $framex -> add ($vboxx);

# SpinButton for starting page number
 my $hboxxs = Gtk2::HBox -> new;
 $vboxx -> pack_start ($hboxxs, FALSE, FALSE, 0);
 my $labelxs = Gtk2::Label -> new ($d->get('Start'));
 $hboxxs -> pack_start($labelxs, FALSE, FALSE, 0);
 $start = 1;
 $spin_buttons = Gtk2::SpinButton -> new_with_range(1, 99999, 1);
 $hboxxs -> pack_end($spin_buttons, FALSE, FALSE, 0);

# SpinButton for page number increment
 my $hboxi = Gtk2::HBox -> new;
 $vboxx -> pack_start ($hboxi, FALSE, FALSE, 0);
 my $labelxi = Gtk2::Label -> new ($d->get('Increment'));
 $hboxi -> pack_start($labelxi, FALSE, FALSE, 0);
 $spin_buttoni = Gtk2::SpinButton -> new_with_range(-99, 99, 1);
 my $step = 1;
 $spin_buttoni -> set_value($step);
 $hboxi -> pack_end($spin_buttoni, FALSE, FALSE, 0);
 $spin_buttoni -> signal_connect ('value-changed' => sub {
  $spin_buttoni -> set_value(-$step) if ($spin_buttoni -> get_value == 0);
  $step = $spin_buttoni -> get_value;
 });

# Check whether the start page exists
 $spin_buttons -> signal_connect ('value-changed' => \&update_start);

# Setting this here to fire callback running update_start
 $spin_buttons->set_value($start);

# Callback on changing number of pages
 $spin_buttonn -> signal_connect ('value-changed' => sub {
  $bscannum -> set_active(TRUE); # Set the radiobutton active

# Check that there is room in the list for the number of pages
  update_number();
 });

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

# Single sided button
 my $buttons = Gtk2::RadioButton -> new(undef, $d->get('Single sided'));
 $tooltips -> set_tip ($buttons, $d->get('Source document is single-sided'));
 $vboxs -> pack_start($buttons, TRUE, TRUE, 0);
 $buttons -> signal_connect (clicked => sub {
  $spin_buttoni -> set_value(1);
 });

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

 my $combobs = Gtk2::ComboBox -> new_text;
 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
  if ($combobs -> get_active == 0) {
   $spin_buttoni -> set_value(2);
  }
  else {
   $spin_buttoni -> set_value(-2);
  }
  if ($#{$slist -> {data}} > -1) {
   $spin_buttons->set_value($slist -> {data}[$#{$slist -> {data}}][0]+1);
  }
  else {
   $spin_buttons->set_value(1);
  }
 });
 $tooltips -> set_tip ($combobs,
              $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 ($combobs, FALSE, FALSE, 0);

# Have to put the double-sided callback here to reference page side
 $buttond -> signal_connect (clicked => sub {
  if ($combobs -> get_active == 0) {
   $spin_buttoni -> set_value(2);
  }
  else {
   $spin_buttoni -> set_value(-2);
  }
  if ($#{$slist -> {data}} > -1) {
   $spin_buttons->set_value($slist -> {data}[$#{$slist -> {data}}][0]+1);
  }
  else {
   $spin_buttons->set_value(1);
  }
 });

# Have to put the extended pagenumber checkbox here to reference simple controls
 $checkx -> signal_connect (toggled => sub {
  if ($checkx -> get_active) {
   $frames->hide_all;
   $framex->show_all;
  }
  else {
   if ($spin_buttoni -> get_value == 1) {
    $buttons -> set_active(TRUE);
   }
   elsif ($spin_buttoni -> get_value > 0) {
    $buttond -> set_active(TRUE);
    $combobs -> set_active(0);
   }
   else {
    $buttond -> set_active(TRUE);
    $combobs -> set_active(1);
   }
   $frames->show_all;
   $framex->hide_all;
  }
 });

# Frame for post-processing
 my $framep = Gtk2::Frame -> new($d->get('Post-processing'));
 $vbox1 -> pack_start ($framep, FALSE, FALSE, 0);
 my $vboxp = Gtk2::VBox -> new;
 $vboxp -> set_border_width($border_width);
 $framep -> add ($vboxp);

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

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

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

# Update $SETTING
   get_unpaper_options($unpaper_options);

   $windowo -> destroy;
  });

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

  $windowo -> show_all;
 });

# CheckButton for OCR
 my $hboxo = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxo, FALSE, FALSE, 0);
 my $obutton = Gtk2::CheckButton -> new($d->get('OCR scanned pages'));
 $tooltips -> set_tip ($obutton, $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);
 $tooltips -> set_tip ($comboboxe, $d->get('Select OCR engine'));
 $hboxo -> pack_end($comboboxe, TRUE, TRUE, 0);
 my ($comboboxl, @tesslang);
 if ($dependencies{tesseract}) {
  (my $hboxl, $comboboxl, @tesslang) = add_ocr_languages($vboxp);
  $hboxl->hide_all
   if ($ocr_engine[$comboboxe -> get_active]->[0] ne 'tesseract');
  $comboboxe -> signal_connect (changed => sub {
   if ($ocr_engine[$comboboxe -> get_active]->[0] eq 'tesseract') {
    $hboxl->show_all;
   }
   else {
    $hboxl->hide_all;
   }
  });
  $hboxl->set_sensitive(FALSE) if (! ($obutton -> get_active));
  $obutton -> signal_connect (toggled => sub {
   if ($obutton -> get_active) {
    $hboxl->set_sensitive(TRUE);
   }
   else {
    $hboxl->set_sensitive(FALSE);
   }
  });
 }

# Frame for device-dependent options
 my $framed = Gtk2::Frame -> new($d->get('Device-dependent options'));
 $vbox2 -> pack_start ($framed, FALSE, FALSE, 0);
 $vboxd = Gtk2::VBox -> new;
 $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
 $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, $vboxm -> 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::ComboBox')) {

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

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

  my $start = $spin_buttons->get_value;
  my $step = $spin_buttoni->get_value;

  if (! $checkx -> get_active and $start == 1 and $step < 0) {
   show_message_dialog($windows, 'error', 'cancel', $d->get('Must scan facing pages first'));
   return TRUE;
  }

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

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

 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'));
  $hboxd->pack_start ($pbar, TRUE, TRUE, 0);
  my $running = TRUE;

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

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

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

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

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

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

# Show window
 $windows -> show_all;
 $vboxm -> hide
  if (defined $vboxm
   and (! defined $combobp or
        ! $combobp -> get_active_text ne $d->get('Manual')));
 $framex->hide_all;

# Has to be done after showing the window, otherwise the window doesn't get centred.
 $sbutton -> set_sensitive(FALSE) if (! @test);
}


sub populate_device_list {
 my ($hboxd, $output) = @_;

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

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

# 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;
 });
 $tooltips -> set_tip ($combobd, $d->get('Sets the device to be used for the scan'));
 $hboxd -> pack_end ($combobd, FALSE, FALSE, 0);

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

# If device in settings then set it
 my $o;
 if (defined $SETTING{device}) {
  for (my $i = 0; $i < @device; $i++) {
   $o = $i if ($SETTING{device} eq $device[$i]);
  }
 }
 if (! defined ($o)) {
  $o = 0;
  delete $SETTING{mode};
 }

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


# Called either from changed-value signal of spinbutton,
# or row-changed signal of simplelist

sub update_start {
 return if (! defined $spin_buttons);
 my $exists = TRUE;
 my $value = $spin_buttons->get_value;
 my $step = $value - $start;
 $step = $spin_buttoni->get_value if ($step == 0);
 my $i = $step > 0 ? 0 : $#{$slist -> {data}};
 $start = $value;
 while ($exists) {
  if ($i < 0 or $i > $#{$slist -> {data}}
      or ($slist -> {data}[$i][0] > $value and $step > 0)
      or ($slist -> {data}[$i][0] < $value and $step < 0)) {
   $exists = FALSE;
  }
  elsif ($slist -> {data}[$i][0] == $value) {
   $value += $step;
   if ($value < 1) {
    $value = 1;
    $step = 1;
   }
  }
  else {
   $i += $step > 0 ? 1 : -1;
  }
 }
 $spin_buttons -> set_value($value) if ($start != $value);
 $start = $value;

 update_number() if ($bscannum -> get_active);
}


# Check how many pages could be scanned

sub pages_possible {
 my $n = 1;
 my $i = $#{$slist -> {data}};
 my $start = $spin_buttons->get_value;
 my $step = $spin_buttoni->get_value;
 my $exists;
 while (! defined $exists) {
  if ($start+$n*$step < 1) {
   $exists = TRUE;
  }
  elsif ($i < 0 and $step < 0) {
   ++$n;
  }
  elsif ($i > $#{$slist -> {data}} or $i < 0) {
   $exists = FALSE;
   $n = -1
  }
  elsif ($slist->{data}[$i][0] == $start+$n*$step) {
   $exists = TRUE;
  }
  elsif ($slist->{data}[$i][0] > $start+$n*$step and $step < 0) {
   --$i;
  }
  elsif ($slist->{data}[$i][0] < $start+$n*$step and $step > 0) {
   ++$i;
  }
  else {
   ++$n;
  }
 }
 return $n;
}


# Update the number of pages to scan spinbutton if necessary

sub update_number {
 my $n = pages_possible();
 $spin_buttonn->set_value($n) if ($n > 0 and $n < $spin_buttonn->get_value);
}


# 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';
 push @options, '--progress';

# 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 $pbar = Gtk2::ProgressBar->new;
  $pbar->set_text ($d->get('Scanning'));
  $dialog -> vbox -> add ($pbar);

# Ensure that the dialog box is destroyed when the user responds.
  $dialog -> signal_connect (response => sub {
   $_[0] -> destroy;
   local $SIG{INT} = 'IGNORE';
   kill INT => $pid;
   $cancel = TRUE;
   undef(@unpaper_stack);
   undef(@ocr_stack);
  });
  $dialog -> show_all;
 
  my $line;
  $scanwatch = Glib::IO->add_watch(fileno($error), ['in', 'hup'], sub {
   my ($fileno, $condition) = @_;
   my $buffer;
   if ($condition & 'in') { # bit field operation. >= would also work

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

    while ($line =~ /^Progress: (\d*\.\d*)%\r/) {
     my $fraction = $1/100;
     $pbar->set_fraction ($fraction);
     $line = substr($line, index($line, "\r")+1, length($line))
    }
    while ($line =~ /\n/) {
     if ($line =~ /^Scanning (-?\d*) pages/) {
      $pbar -> set_text($d->get('Scanning')." $1 ".$d->get('pages')."...");
     }
     elsif ($line =~ /^Scanning page (\d*)/) {
      $pbar -> set_text(sprintf($d->get('Scanning page %i...'), $1*$step+$offset));
      $pbar->set_fraction (0);
     }
     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|wait for lamp warm-up/) {
      $pbar -> 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"));
      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 $pbar = Gtk2::ProgressBar->new;
  $pbar -> set_text($d->get('Scanning')."...");
  $pbar -> set_pulse_step(.1);
  $dialog -> vbox -> add ($pbar);
  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;
                                             } });

# 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;
   $running = FALSE;
  });
  $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;
    print $buffer if ($debug and $buffer);
    $line .= $buffer;

    while ($line =~ /\n/) {
     if ($line =~ /Scanner warming up - waiting \d* seconds|wait for lamp warm-up/) {
      $pbar -> set_text($d->get('Scanner warming up'));
     }
     elsif ($line =~ /^Scanned document out(\d*)\.pnm/) {
      $pbar -> 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;
       $running = FALSE;
       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;
      $running = FALSE;
      show_message_dialog($windows, 'error', 'close', $d->get('Device busy'));
     }
     else {
      my $text = $d->get('Unknown message: ')
                                         . substr($line, 0, index($line, "\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;
    $running = FALSE;
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  warn "$cmd\n";
 }
}


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

sub hash2options {
 my %options = @_;

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

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

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


# Take new scan and display it

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

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

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

 return TRUE;
}


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

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

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

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

 my (undef, $filename) = tempfile(DIR => $dir, SUFFIX => $suffix{$format});
 if (defined($delete) and $delete) {
  move($ofilename, $filename) or 
   show_message_dialog($window, 'error', 'close', $d->get('Error importing image'));
 }
 else {
  copy($ofilename, $filename) or 
   show_message_dialog($window, 'error', 'close', $d->get('Error importing image'));
 }

 return $filename, $resolution;
}


# Take new scan and display it

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

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

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

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

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

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

 return @page;
}


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


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

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

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

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

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

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


# Cut the selection

sub cut_selection {
 if ($slist -> has_focus) {
  copy_selection();
  delete_pages();
 }
 elsif ($textview -> has_focus) {
  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  $textbuffer->cut_clipboard ($clipboard, TRUE)
 }
}


# Copy the selection

sub copy_selection {
 if ($slist -> has_focus) {
  undef @clipboard;
  my @pages = $slist -> get_selected_indices;
  for my $page (@pages) {
   my @copy = map { [ @$_ ] } $slist->{data}[$page];
   push @clipboard, @copy;
  }
 }
 elsif ($textview -> has_focus) {
  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  $textbuffer->copy_clipboard ($clipboard)
 }
}


# Paste the selection

sub paste_selection {
 if ($slist -> has_focus) {
  my @page = $slist -> get_selected_indices;

# Create a new image file
  for (@clipboard) {
   my $suffix = $1 if ($_->[2] =~ /(\.\w*)$/);
   my (undef, $new) = tempfile(DIR => $dir, SUFFIX => $suffix);
   copy($_->[2], $new) or 
    show_message_dialog($window, 'error', 'close', $d->get('Error pasting image'));
   $_->[2] = $new;
  }

# Block the row-changed signal whilst adding the scan (row) and sorting it.
  $slist -> get_model -> signal_handler_block($slist -> {signalid});

  if (@page) {
   splice @{ $slist->{data} }, $page[0]+1, 0, @clipboard;
   @page = ( $page[0]+1 );
  }
  else {
   push @{$slist -> {data}}, @clipboard;
   @page = ( $#{$slist -> {data}} - $#clipboard );
  }

  renumber ($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;
  $slist -> select(@page);

  update_uimanager();
 }
 elsif ($textview -> has_focus) {
  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  $textbuffer->paste_clipboard ($clipboard, undef, TRUE)
 }
}


# Delete the selected scans

sub delete_pages {

# Update undo/redo buffers
 take_snapshot();

 my @pages = $slist -> get_selected_indices;
 my @page = @pages;
 while (@pages) {
  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);
 }

# Reset start page in scan dialog
 reset_start();

 update_uimanager();
}


# Reset start page number after delete or new

sub reset_start {
 if (defined $spin_buttons) {
  if ($#{$slist->{data}} > -1) {
   my $start = $spin_buttons->get_value;
   my $step = $spin_buttoni->get_value;
   $spin_buttons->set_value($slist->{data}[$#{$slist->{data}}][0] + $step)
    if ($start > $slist->{data}[$#{$slist->{data}}][0] + $step);
  }
  else {
   $spin_buttons->set_value(1);
  }
 }
}


# 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 {
 use utf8;
 my $about = Gtk2::AboutDialog->new;
# Gtk2::AboutDialog->set_url_hook ($func, $data=undef);
# Gtk2::AboutDialog->set_email_hook ($func, $data=undef);
 $about->set_program_name ($program);
 $about->set_version ($version);
 my $authors = <<EOS;
John Goerzen
Chris Mayo
David Hampton
Sascha Hunold
EOS
 $about->set_authors ("Jeff Ratcliffe\n\n"
                      .$d->get('Patches gratefully received from:')
		      ."\n$authors");
 $about->set_comments ($d->get('To aid the scan-to-PDF process'));
 $about->set_copyright ($d->get('Copyright 2006--2008 Jeffrey Ratcliffe'));
 my $licence = <<EOS;
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright (C) 2006 -- 2008  Jeffrey Ratcliffe <Jeffrey.Ratcliffe\@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the version 3 GNU General Public License as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
EOS
 $about->set_license ($licence);
 $about->set_website ('http://gscan2pdf.sf.net');
 my $translators = <<EOS;
jimpap
Petr Jelinek
Matthias Gutjahr
Alexandre Prokoudine
Nicolas Stransky
Piotr Strębski
Hugo Pereira
Daniel Frank
Dirk Tas
Matthias Mailänder
Jen Ockwell
Yözen Hernández
Christoph Langner
Spartakus
Jacob Nielsen
Alberto Boiti
Yves MATHIEU
luther-x
Ioannis Koniaris
EOS
 $about->set_translator_credits ($translators);
 $about->set_artists ('lodp');
 $about->run;
 $about->destroy;
}


# Check that a command exists

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


# Rescan device-dependent scan options

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

#  Ghost the scan button whilst options being updated
 $sbutton->set_sensitive(FALSE) if (defined $sbutton);

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

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

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

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

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

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

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

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

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


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

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

# Dig out the paper sizes
 my ($x, $y, $l, $t, $h, $w, $spin_buttonx, $spin_buttony, $spin_buttonl, $spin_buttont, $spin_buttonh, $spin_buttonw);
 $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);

  $combobp = Gtk2::ComboBox -> new_text;
  $combobp -> append_text ($d->get('Manual'));
  $combobp -> append_text ($d->get('Edit'));
  $tooltips -> set_tip ($combobp, $d->get('Selects or edits the paper size'));
  $hboxp -> pack_end ($combobp, FALSE, FALSE, 0);

# Define manual paper here to reference it in callback
  $vboxm = Gtk2::VBox -> new;
  $vboxd -> pack_start ($vboxm, FALSE, FALSE, 0);
 }

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

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

   if ($option eq 'pagewidth') {
    $w = $hash{$option}{max};
    $spin_buttonw = $widget;
    $vboxm -> pack_start ($hbox, TRUE, TRUE, 0);
   }
   elsif ($option eq 'pageheight') {
    $h = $hash{$option}{max};
    $spin_buttonh = $widget;
    $vboxm -> pack_start ($hbox, TRUE, TRUE, 0);
   }
   else {
    $vboxd -> pack_start ($hbox, TRUE, TRUE, 0);
   }

# If an ADF isn't selected, then we don't want to scan all pages
   if ($option eq 'source') {
    $widget -> signal_connect (changed => sub {
     my $text = $widget -> get_active_text;
     if (defined($text) and
                     get_value(\%ddo, $option, $text) =~ /(Flatbed)|(Normal)/) {
      $bscanall->set_sensitive(FALSE);
      $bscannum->set_active(TRUE);
     }
     else {
      $bscanall->set_sensitive(TRUE);
     }
    });
   }

# If the mode is changed, update the options
   if ($option eq 'mode') {
    $widget -> signal_connect (changed => sub {
     $SETTING{$option} = get_value(\%ddo, $option, $widget -> get_active_text);
     update_options($vboxd, $device);
    });
   }
  }
 }

# Add paper size after rest of options in case of pagewidth|height
 if (defined($x) and defined($y)) {
  $x = $w if (defined $w);
  $y = $h if (defined $h);
  my @info = ( { label => $d->get('Width'),
                 tooltip => $d->get('Width of scan area'),
                 letter => 'x',
                 dimension => $x },
               { label => $d->get('Height'),
                 tooltip => $d->get('Height of scan area'),
                 letter => 'y',
                 dimension => $y },
               { label => $d->get('Left'),
                 tooltip => $d->get('Top-left x position of scan area'),
                 letter => 'l',
                 dimension => $l },
               { label => $d->get('Top'),
                 tooltip => $d->get('Top-left y position of scan area'),
                 letter => 't',
                 dimension => $t } );
  ($spin_buttonx, $spin_buttony, $spin_buttonl, $spin_buttont) = add_spinbutton($vboxm, @info);
  $combobp -> signal_connect (changed => sub {
   if ($combobp -> get_active_text eq $d->get('Edit')) {
    edit_paper($combobp, $x, $y, $l, $t, $h, $w);
   }
   elsif ($combobp -> get_active_text eq $d->get('Manual')) {
    $vboxm -> show_all;
   }
   else {
    my $paper = $combobp -> get_active_text;
    $spin_buttonx -> set_value($SETTING{Paper}{$paper}{x});
    $spin_buttony -> set_value($SETTING{Paper}{$paper}{y});
    $spin_buttont -> set_value($SETTING{Paper}{$paper}{t});
    $spin_buttonl -> set_value($SETTING{Paper}{$paper}{l});
    if (defined($spin_buttonh) and defined($spin_buttonw)) {
     $spin_buttonw -> set_value($SETTING{Paper}{$paper}{x});
     $spin_buttonh -> set_value($SETTING{Paper}{$paper}{y});
    }
    $vboxm -> hide;
    $windows -> resize(200, 200); # Doesn't matter that 200x200 is too small
   }
  });
  add_paper($combobp, $x, $y, $l, $t, $h, $w);
  set_paper($combobp);
 }

# Show window
 $vboxd -> show_all;
 $vboxm -> hide if ($combobp -> get_active_text ne $d->get('Manual'));

#  Unghost the scan button
 $sbutton->set_sensitive(TRUE) if (defined $sbutton);
}


# take an option from the options hash and create the appropriate widget

sub add_widget_from_hash {
 my ($option, %hash) = @_;

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

# HBox for option
 my $hbox = Gtk2::HBox -> new;
 $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);

# Widget
 my $widget;

# SpinButton
 if (defined $hash{$option}{max}) {
  my $step = 1;
  $step = $hash{$option}{step} if (defined $hash{$option}{step});
  $widget = Gtk2::SpinButton -> new_with_range($hash{$option}{min},
                                                    $hash{$option}{max}, $step);

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

# ComboBox
 else {
  $widget = Gtk2::ComboBox -> new_text;

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

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

 $hbox -> pack_end ($widget, FALSE, FALSE, 0);
 $tooltips -> set_tip ($widget, $hash{$option}{tip});
 return ($widget, $hbox);
}


# Helper function to get manual paper size spinbuttons on dialog

sub add_spinbutton {
 my ($vbox, @info) = @_;
 my @stack;
 for (@info) {
  my $hbox = Gtk2::HBox -> new;
  $vbox -> pack_start ($hbox, FALSE, FALSE, 0);
  my $label = Gtk2::Label -> new ($_->{label});
  $hbox -> pack_start ($label, FALSE, FALSE, 0);
  my $spinbutton = Gtk2::SpinButton -> new_with_range(0, $_->{dimension}, 1);
  $spinbutton -> set_value($SETTING{$_->{letter}});
  $tooltips -> set_tip ($spinbutton, $_->{tooltip});
  $hbox -> pack_end ($spinbutton, FALSE, FALSE, 0);
  push @stack, $spinbutton;
 }
 return @stack;
}


# Paper editor
sub edit_paper {
 my ($combobp, $x, $y, $l, $t, $h, $w) = @_;

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

 ($windowd, my $vbox) = create_window($windows, $d->get('Edit paper size'), TRUE);

# Buttons for SimpleList
 my $hboxl = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxl, FALSE, FALSE, 0);
 my $vboxb = Gtk2::VBox -> new;
 $hboxl -> pack_start ($vboxb, FALSE, FALSE, 0);
 my $dbutton = Gtk2::Button -> new_from_stock('gtk-add');
 $vboxb -> pack_start ($dbutton, TRUE, FALSE, 0);
 my $rbutton = Gtk2::Button -> new_from_stock('gtk-remove');
 $vboxb -> pack_end ($rbutton, TRUE, FALSE, 0);
    
# Set up a SimpleList
 my $slist = Gtk2::Ex::Simple::List -> new($d->get('Name') => 'text',
                                           $d->get('Width') => 'int',
                                           $d->get('Height') => 'int',
                                           $d->get('Left') => 'int',
                                           $d->get('Top') => 'int');
 for (keys %{$SETTING{Paper}}) {
  push @{ $slist->{data} }, [ $_,
                              $SETTING{Paper}{$_}{x},
                              $SETTING{Paper}{$_}{y},
                              $SETTING{Paper}{$_}{l},
                              $SETTING{Paper}{$_}{t} ];
 }

# Set everything to be editable
 for (0..4) {
  $slist -> set_column_editable($_, TRUE);
 }
 $slist->get_column(0)->set_sort_column_id(0);

# Add button callback
 $dbutton -> signal_connect (clicked => sub {
  my @rows = $slist -> get_selected_indices;
  $rows[0] = 0 if (! @rows);
  my $name = $slist->{data}[$rows[0]][0];
  my $version = 2;
  my $i = 0;
  while ($i < @{$slist->{data}}) {
   if ($slist->{data}[$i][0] eq "$name ($version)") {
    ++$version;
    $i = 0;
   }
   else {
    ++$i;
   }
  }
  my @line = [ "$name ($version)",
               $slist->{data}[$rows[0]][1],
               $slist->{data}[$rows[0]][2],
               $slist->{data}[$rows[0]][3],
               $slist->{data}[$rows[0]][4] ];
  splice @{ $slist->{data} }, $rows[0]+1, 0, @line;
 });   

# Remove button callback
 $rbutton -> signal_connect (clicked => sub {
  my @rows = $slist -> get_selected_indices;
  if ($#rows == $#{$slist->{data}}) {
   show_message_dialog($windowd, 'error', 'close', $d->get('Cannot delete all paper sizes'));
  }
  else {
   while (@rows) {
    splice @{ $slist->{data} }, shift(@rows), 1;
   }
  }
 });   

# Set-up the callback to check that no two Names are the same
 $slist -> get_model -> signal_connect('row-changed' => sub {
  my ($model, $path, $iter) = @_;
  for (my $i = 0; $i < @{$slist->{data}}; $i++) {
   if ($i != $path->to_string and
        $slist->{data}[$path->to_string][0] eq $slist->{data}[$i][0]) {
    my $name = $slist->{data}[$path->to_string][0];
    my $version = 2;
    if ($name =~ /(.*) \((\d+)\)/) {
     $name = $1;
     $version = $2+1;
    }
    $slist->{data}[$path->to_string][0] = "$name ($version)";
    return;
   }
  }
 });
 $hboxl -> pack_end($slist, FALSE, FALSE, 0);

# Buttons
 my $hboxb = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxb, FALSE, FALSE, 0);
 my $abutton = Gtk2::Button -> new_from_stock('gtk-apply');
 $abutton -> signal_connect( clicked => sub {
  delete $SETTING{Paper};
  for (my $i = 0; $i < @{$slist->{data}}; $i++) {
   $SETTING{Paper}{$slist->{data}[$i][0]}{x} = $slist->{data}[$i][1];
   $SETTING{Paper}{$slist->{data}[$i][0]}{y} = $slist->{data}[$i][2];
   $SETTING{Paper}{$slist->{data}[$i][0]}{l} = $slist->{data}[$i][3];
   $SETTING{Paper}{$slist->{data}[$i][0]}{t} = $slist->{data}[$i][4];
  }
  $combobp->remove_text(0) while ($combobp->get_active > 1);
  my @ignored = add_paper($combobp, $x, $y, $l, $t, $h, $w);
  show_message_dialog($windowd, 'warning', 'close',
   $d->get('The following paper sizes are too big to be scanned by the selected device:')
    .' '.join(', ', @ignored)) if (@ignored);
  my @rows = $slist -> get_selected_indices;
  $rows[0] = 0 if (! @rows);
  $SETTING{'Paper size'} = $slist->{data}[$rows[0]][0];
  set_paper($combobp);
  $windowd -> hide;
 });
 $hboxb -> pack_start ($abutton, TRUE, FALSE, 0);
 my $cbutton = Gtk2::Button -> new_from_stock('gtk-cancel');
 $cbutton -> signal_connect( clicked => sub {
  my @rows = $slist -> get_selected_indices;
  $rows[0] = 0 if (! @rows);
  $SETTING{'Paper size'} = $slist->{data}[$rows[0]][0];
  set_paper($combobp);
  $windowd -> hide;
 });
 $hboxb -> pack_end ($cbutton, TRUE, FALSE, 0);
 $windowd->show_all;
}


# Add paper size to combobox if scanner large enough

sub add_paper {
 my ($combobox, $x, $y, $l, $t, $h, $w) = @_;
 my @ignored;
 for (keys %{$SETTING{Paper}}) {
  if (defined($h) and defined($w)
      and $h >= $SETTING{Paper}{$_}{y}+$SETTING{Paper}{$_}{t}
      and $w >= $SETTING{Paper}{$_}{x}+$SETTING{Paper}{$_}{l}) {
   $combobox -> prepend_text ($_);
  }
  elsif ($x+$tolerance >= $SETTING{Paper}{$_}{x}+$SETTING{Paper}{$_}{l}
       and $y+$tolerance >= $SETTING{Paper}{$_}{y}+$SETTING{Paper}{$_}{t}
       and $l+$tolerance >= $SETTING{Paper}{$_}{l}
       and $t+$tolerance >= $SETTING{Paper}{$_}{t}) {
   $combobox -> prepend_text ($_);
  }
  else {
   push @ignored, $_;
  }
 }
 return @ignored;
}


# Set default paper size from config

sub set_paper {
 my ($combobox) = @_;
 my $o = 0;
 if (defined($SETTING{'Paper size'})) {
  my $i = 0;
  $combobox->get_model->foreach (sub {
   my ($model, $path, $iter) = @_;
   if ($model->get($iter, 0) eq $SETTING{'Paper size'}) {
    $o = $i;
    return TRUE;
   }
   else {
    ++$i;
    return FALSE;
   }
  });
 }
 $combobox -> set_active($o);
}


# 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));
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
 }
 else {
  my $i = 0;
  $i++ while ($device[$i] ne $device);

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

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


# return a hash of the passed options

sub options2hash {

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

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

# only set the default if it is an option
                      and (defined($hash{$_}{max})
              or defined(default2index($SETTING{$_}, @{$hash{$_}{values}})))) {
   $hash{$_}{default} = $SETTING{$_};
  }

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


# walk the widget tree and update them from the hash

sub update_options_hash {

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

 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::ComboBox')
# to prevent recursion
                                            and $key ne 'mode'
                                            and defined($hash{$key}{values})) {
     
# Empty the list
     $widget->get_model->clear;

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

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

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

     $hbox -> show_all;
    }
    elsif ($key eq 'mode') {
     $hboxm = $hbox;
    }
    elsif ($key eq 'Paper size') {
     $hboxp = $hbox;
    }
    elsif ($widget -> isa('Gtk2::SpinButton') and defined($hash{$key}{max})) {
     $widget->set_range($hash{$key}{min}, $hash{$key}{max});

# Set the default
     if ($hash{$key}{default} =~ /inactive/) {
      $hbox->set_sensitive(FALSE);
     }
     else {
      $widget->set_value($hash{$key}{default});
      $hbox->set_sensitive(TRUE);
     }
     $hbox -> show_all;
    }

# If an option no longer available, make it insensitive so that it doesn't
# get passed to scanimage or scanadf
    else {
     $hbox->set_sensitive(FALSE);
    }
   }

# Delete option from hash to see at the end which are left
   delete $hash{$key} if (defined $hash{$key});
  }
 }

# Add any new options
 foreach my $option (keys %hash) {
  if (defined($pddo{$option}) and $option !~ /^page(width)|(height)/) {
   my ($widget, $hbox) = add_widget_from_hash($option, %hash);
   $vboxd -> pack_start ($hbox, TRUE, TRUE, 0);
   $hbox -> show_all;
  }
 }

 $vboxm -> show_all if ($combobp -> get_active_text eq $d->get('Manual'));
 $hboxm -> show_all if (defined $hboxm);
 $hboxp -> show_all if (defined $hboxp);
 $vboxd -> show;
}


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


# Renumber pages

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

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

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

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

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


# Rotate image

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

# Update undo/redo buffers
 take_snapshot();

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

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

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

  $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";

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

# allow event propagation
 return FALSE;
}


# Display page selector and on apply threshold accordingly

sub threshold {

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

 my ($windowt, $vbox) = create_window($window, $d->get('Threshold'), TRUE);

# Frame for page range
 add_page_range($vbox);

# SpinButton for threshold
 my $hboxt = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxt, FALSE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Threshold'));
 $hboxt -> pack_start ($label, FALSE, TRUE, 0);
 my $labelp = Gtk2::Label -> new ($d->get('%'));
 $hboxt -> pack_end ($labelp, FALSE, TRUE, 0);
 my $spinbutton = Gtk2::SpinButton -> new_with_range(0, 100, 1);
 $spinbutton->set_value ($SETTING{'threshold tool'});
 $hboxt -> pack_end ($spinbutton, FALSE, TRUE, 0);

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

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

# Update undo/redo buffers
  take_snapshot();

  $SETTING{'threshold tool'} = $spinbutton->get_value;
  my $dialog = Gtk2::Dialog -> new ($d->get('Applying threshold')."...", $windowo,
                                            'destroy-with-parent',
                                            'gtk-cancel' => 'cancel');

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

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

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

  my @pagelist = get_page_index();
  my $pid = start_process(sub {

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

   foreach (@pagelist) {
    send($writer, sprintf("%i %i", $page, $#pagelist+1), 0);

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

# Threshold the image
    $image->BlackThreshold(threshold=>$SETTING{'threshold tool'}.'%');
    $image->WhiteThreshold(threshold=>$SETTING{'threshold tool'}.'%');

# Write it
    my $suffix = $1 if ($filename =~ /\.(\w*)$/);
    (undef, $filename) = tempfile(DIR => $dir, SUFFIX => '.'.$suffix);
    $x = $image->Write(filename => $filename);
    warn "$x" if "$x";
    warn "Thresholding at $SETTING{'threshold tool'} to $filename\n" if $debug;

    send($writer, sprintf("%i %i %s", $page, $#pagelist+1, $filename), 0);
    ++$page;
   }
   send($writer, '2 1', 0);
  });

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if ($line =~ /(\d*) (\d*) ?(.*)/) {
     my $page=$1;
     my $total=$2;
     my $filename=$3;
     my $fraction = $page/$total;
     if ($fraction > 1) {
      $dialog -> destroy;

# Reselect the pages to display the rotated image(s)
      $slist->select(@pagelist);
      return FALSE;  # uninstall
     }
     if (defined $filename and $filename ne '') {
      $slist -> {data}[$pagelist[$page]][2] = $filename;
      $slist -> {data}[$pagelist[$page]][1] =
           get_pixbuf($slist -> {data}[$pagelist[$page]][2], $heightt, $widtht);
     }
     else {
      $pbar->set_fraction($fraction);
      $pbar->set_text(sprintf($d->get("Applying threshold to page %i of %i"),
                                                              $page+1, $total));
     }
    }
   }

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

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

 $windowt -> show_all;
}


# Display page selector and on apply threshold accordingly

sub crop {

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

 my ($windowc, $vbox) = create_window($window, $d->get('Crop'), TRUE);

# Frame for page range
 add_page_range($vbox);

# SpinButton for threshold
 my $hboxt = Gtk2::HBox -> new;
 $vbox -> pack_start ($hboxt, FALSE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Crop'));
 $hboxt -> pack_start ($label, FALSE, TRUE, 0);
 my $labelp = Gtk2::Label -> new ($d->get('%'));
 $hboxt -> pack_end ($labelp, FALSE, TRUE, 0);
 my $spinbutton = Gtk2::SpinButton -> new_with_range(0, 100, 1);
 $spinbutton->set_value ($SETTING{'threshold tool'});
 $hboxt -> pack_end ($spinbutton, FALSE, TRUE, 0);

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

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

  return if (! $eventbox->{draginfo});

# Update undo/redo buffers
  take_snapshot();

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

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

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

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

  my @pagelist = get_page_index();
  my $pid = start_process(sub {

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

# Erase box
#   draw_rubber_band_box ($eventbox);

   foreach (@pagelist) {
    send($writer, sprintf("%i %i", $page, $#pagelist+1), 0);

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

# Image dimensions
    my $height = $image->Get('height');
    my $width = $image->Get('width');
print "image $height x $width = ", $height/$width, "\n";

# ScrolledWindow dimensions
    my $widths = $scwin_detail -> allocation -> width;
    my $heights = $scwin_detail -> allocation -> height;
print "ScrolledW $heights x $widths = ", $heights/$widths, "\n";

# Viewport (hence ->child) dimensions
    my $widthv = $scwin_detail -> child -> allocation -> width;
    my $heightv = $scwin_detail -> child -> allocation -> height;
print "Viewport $heightv x $widthv = ", $heightv/$widthv, "\n";

# Eventbox dimensions
    my $widthe = $scwin_detail -> child -> child -> allocation -> width;
    my $heighte = $scwin_detail -> child -> child -> allocation -> height;
print "EventB $heighte x $widthe = ", $heighte/$widthe, "\n";

    my $factor = ($height/$width > $heighte/$widthe)
                                            ? $height/$heighte : $width/$widthe;

# If we have scrollbars, measure from top left, otherwise measure from centre
    my $xoffset = $heightv == $heights ? $width/2 - $widthe/2 * $factor : 0;
    my $yoffset = $widthv == $widths ? $height/2 - $heighte/2 * $factor : 0;
print "xoffset $xoffset, yoffset $yoffset, factor $factor\n";

# Convert crop box from viewport to image coordinates
    my $x1 = $eventbox->{draginfo}{x1};
    my $x2 = $eventbox->{draginfo}{x2};
    my $y1 = $eventbox->{draginfo}{y1};
    my $y2 = $eventbox->{draginfo}{y2};
    my $x = min($x1, $x2) * $factor + $xoffset;
    my $y = min($y1, $y2) * $factor + $yoffset;
    my $w = abs($x2 - $x1) * $factor;
    my $h = abs($y2 - $y1) * $factor;

# Crop the image
    warn "Cropping $w x $h + $x + $y\n" if $debug;
    $e = $image->Crop(width=>$w, height=>$h, x=>$x, y=>$y);
    warn "$e" if "$e";

# Write it
    my $suffix = $1 if ($filename =~ /\.(\w*)$/);
    (undef, $filename) = tempfile(DIR => $dir, SUFFIX => '.'.$suffix);
    warn " to $filename\n" if $debug;
    $e = $image->Write(filename => $filename);
    warn "$e" if "$e";

    send($writer, sprintf("%i %i %s", $page, $#pagelist+1, $filename), 0);
    ++$page;
   }
   send($writer, '2 1', 0);
  });

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($reader, $line, 1000, 0);
    if ($line =~ /(\d*) (\d*) ?(.*)/) {
     my $page=$1;
     my $total=$2;
     my $filename=$3;
     my $fraction = $page/$total;
     if ($fraction > 1) {
      $dialog -> destroy;

# Reselect the pages to display the rotated image(s)
      $slist->select(@pagelist);
      return FALSE;  # uninstall
     }
     if (defined $filename and $filename ne '') {
      $slist -> {data}[$pagelist[$page]][2] = $filename;
      $slist -> {data}[$pagelist[$page]][1] =
           get_pixbuf($slist -> {data}[$pagelist[$page]][2], $heightt, $widtht);
     }
     else {
      $pbar->set_fraction($fraction);
      $pbar->set_text(sprintf($d->get("Cropping page %i of %i"), $page+1, $total));
     }
    }
   }

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

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

 $windowc -> show_all;
}


# guess from which window the sub was called

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


sub get_unpaper_options {
 my $hashref = shift;

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


sub options2unpaper {
 my $hashref = $unpaper_options;

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


# Run unpaper to clean up scan.

sub unpaper {

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

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

# Frame for page range
 add_page_range($vbox);

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

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

# Update undo/redo buffers
  take_snapshot();

# Update $SETTING
  get_unpaper_options($unpaper_options);

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

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

 $windowu -> show_all;
}


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

sub ocr_page {
 push @ocr_stack, @_;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


# Have to roll my own slurp sub to support utf8

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

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


# Create a combobox from an array and set the default

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

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

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


# Add hbox for ocr languages

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

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

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

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

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

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


# Run OCR on current page and display result

sub OCR {

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

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

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

# Frame for page range
 add_page_range($vbox);

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

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

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

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

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

 $windowo -> show_all;
}


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

sub quit {

# Check that all pages have been saved
 for (my $i = 0; $i < @{$slist -> {data}}; $i++) {
  if (! $slist -> {data}[$i][5]) {
   my $response = show_message_dialog($window, 'question', 'ok-cancel',
       $d->get("Some pages have not been saved.\nDo you really want to quit?"));
   if ($response ne 'ok') {
    return FALSE;
   }
   else {
    last;
   }
  }
 }

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

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

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

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

# delete $SETTING{'RMB'};

# Write config file
 $conf->save_file($rc, \%SETTING);

 kill_subs();
 return TRUE;
}


# View POD

sub view_pod {

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

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

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

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

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

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

# Viewer
 my $viewer = Gtk2::Ex::PodViewer->new;
 $viewer->set_border_width($border_width);
 $viewer->set_cursor_visible(FALSE);
 $index->get_selection->signal_connect('changed', sub {
  my $idx = ($index->get_selected_indices)[0];
  my $mark = $index->{data}[$idx][2];
  $viewer->jump_to($mark);
  return TRUE;
 });

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

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

 $viewer -> load($0);

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

 $windowh -> show_all;
}


# Remove formatting characters

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


# Add option, value pair to options

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

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


# Get option string from label

sub get_key {
 my ($options, $value) = @_;
 foreach (keys %$options) {
  return $_ if ($options->{$_}{string} eq $value);
 }
 return undef;
}


# Get value string from combobox

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


# 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;
 warn Dumper(\@undo_buffer) if $debug;

# 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;
 warn Dumper(\@redo_buffer, \@undo_buffer) if $debug;

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

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

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


# Fork the passed sub

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

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

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

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

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

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

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

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


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

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


#sub min { $_[0] < $_[1] ? $_[0] : $_[1] }


# rubber band for crop tool

#sub draw_rubber_band_box {
# my ($widget) = @_;
# my $x = min ($widget->{draginfo}{x1}, $widget->{draginfo}{x2});
# my $y = min ($widget->{draginfo}{y1}, $widget->{draginfo}{y2});
# my $w = abs ($widget->{draginfo}{x2} - $widget->{draginfo}{x1});
# my $h = abs ($widget->{draginfo}{y2} - $widget->{draginfo}{y1});
# $widget->window->draw_rectangle($widget->{drag_gc}, FALSE, $x, $y, $w, $h);
#}


# marked page list as saved

sub mark_pages {
 $slist -> get_model -> signal_handler_block($slist -> {signalid});
 foreach (@_) {
  $slist -> {data}[$_][5] = TRUE;
 }
 $slist -> get_model -> signal_handler_unblock($slist -> {signalid});
}


# Convert all files in temp that are not jpg, png, tiff to tiff,
# deleting those no longer needed

sub compress_temp {
 return
  if (show_message_dialog($window, 'question', 'ok-cancel',
   $d->get('This operation cannot be undone. Are you sure?')) ne 'ok');
 @undo_buffer = ();
 @undo_selection = ();
 $uimanager->get_widget('/MenuBar/Edit/Undo') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo') -> set_sensitive(FALSE);

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

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

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

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

 my @filename;
 for (my $i = 0; $i < @{$slist -> {data}}; $i++) {
  $filename[$i] = $slist -> {data}[$i][2];
 }

 my $pid = start_process(sub {
  for (my $i = 0; $i < @filename; $i++) {

# Update the list in the fork
   $filename[$i] = convert_to_tiff($filename[$i]);

# Pass the new filename back to the GUI
   send($writer, $i/@filename." $i $filename[$i] "
                                             .$d->get('Compressing images'), 0);
  }

# make a hash with a list of the files in temp
  my %files;
  send($writer, "0 -1 x ".$d->get('Clearing up'), 0);
  foreach (<$dir/*>) {
   $files{$_} = TRUE;
  }
  foreach (@filename) {
   delete $files{$_} if (defined $files{$_});
  }
  my @files = keys %files;
  my $i = 1;
  foreach (@files) {
   send($writer, $i++/@files." -1 $_ ".$d->get('Clearing up'), 0);
   print "Deleting $_\n" if $debug;
   unlink $_;
  }
  send($writer, '2', 0);
 });

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($reader, $line, 100, 0);
   if (defined($line) and $line ne '') {
    my ($fraction, $page, $filename, @text) = split ' ', $line;
    if ($fraction > 1) {
     $dialog -> destroy;
     return FALSE;  # uninstall
    }
    else {
     $pbar->set_fraction($fraction);
     $pbar->set_text(join ' ', @text);
     Gtk2->main_iteration while Gtk2->events_pending;
     if ($page > -1) {
      $slist -> get_model -> signal_handler_block($slist -> {signalid});
      unlink $slist -> {data}[$page][2];
      $slist -> {data}[$page][2] = $filename;
      $slist -> get_model -> signal_handler_unblock($slist -> {signalid});
     }
    }
   }
  }

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

}


package Gscan2pdf::PageRange;

use strict;
use warnings;
use Gtk2;
use Glib qw(TRUE FALSE);             # To get TRUE and FALSE

# Note: in a BEGIN block to ensure that the registration is complete
#       by the time the use Subclass goes to look for it.
BEGIN {
   Glib::Type->register_enum ('Gscan2pdf::PageRange::Range',
                               qw(current selected all));
}

# this big hairy statement registers our Glib::Object-derived class
# and sets up all the signals and properties for it.
use Glib::Object::Subclass
    Gtk2::VBox::,
    signals => {
        changed => {},
    },
    properties => [
        Glib::ParamSpec->enum (
               'active', # name
               'active', # nickname
               'Either current, selected, or all', #blurb
               'Gscan2pdf::PageRange::Range',
               'selected', # default
               [qw/readable writable/] #flags
        ),
    ]
    ;


sub INIT_INSTANCE {
 my $self = shift;
 my %buttons = (
  'current' => $d->get('Current'),
  'selected' => $d->get('Selected'),
  'all' => $d->get('All'),
 );
 my $vbox = Gtk2::VBox -> new;
 $self->add ($vbox);

#the first radio button has to set the group,
#which is undef for the first button
 my $group;
 foreach my $nick (keys %buttons) {
  $self->{button}{$nick} = Gtk2::RadioButton -> new($group, $buttons{$nick});
  $self->{button}{$nick} -> signal_connect ('toggled' => sub {
   $self->set_active($nick) if ($self->{button}{$nick} -> get_active);
  });
  $vbox -> pack_start($self->{button}{$nick}, TRUE, TRUE, 0);
  $group = $self->{button}{$nick} -> get_group if (! $group);
  $self->{active} = $nick if (! $self->{active});
 }
}


sub get_active {
 my ($self) = @_;
 return $self->get ('active');
}


sub set_active {
 my ($self, $active) = @_;
 $self->{active} = $active;
 foreach my $nick (keys %{$self->{button}}) {
  if ($self->{active} eq $nick) {
   $self->{button}{$nick} -> set_active(TRUE);
   $self->signal_emit ('changed');
  }
 }
}


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

If your distribution doesn't have C<libgtk2-ex-simple-list-perl>, download it from the website and then install it with:

C<sudo dpkg -i libgtk2-ex-simple-list-perl_0.50-1_all.deb>

=head2 RPMs

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

=head2 From source

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

=head2 From the repository

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

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

=head1 Building gscan2pdf from source

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

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

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

=head1 Dependencies

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

=over

=item Required

=over

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

The GTK+ graphical user interface library.

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

Perl interface to the GLib and GObject libraries

=item libgtk2-ex-simple-list-perl

A simple interface to Gtk2's complex MVC list widget

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

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

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

Using libc functions for internationalization in Perl

=item libpdf-api2-perl

provides the functions for creating PDF documents in Perl

=item libsane

API library for scanners

=item libtiff-tools

TIFF manipulation and conversion tools

=item Imagemagick

Image manipulation programs

=item perlmagick

A perl interface to the libMagick graphics routines

=item sane-utils

API library for scanners -- utilities.

=back

=item Optional

=over

=item sane

scanner graphical frontends. Only required for the scanadf frontend.

=item libgtk2-ex-podviewer-perl

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

=item unpaper

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

=item xdg-utils

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

=item djvulibre-bin

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

=item gocr

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

=item tesseract

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

=back

=back

=head1 Support

There are two mailing lists for gscan2pdf:

=over

=item gscan2pdf-announce

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

=item gscan2pdf-help

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

=back

=head1 Reporting bugs

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

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

Please include the output of C<gscan2pdf --debug> with any new bug report.

=head1 Translations

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

=head1 Menus

=head2 File

=head3 New

Clears the page list.

=head3 Import

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

=head3 Scan

Sets options before scanning via SANE.

=head4 Device

Chooses between available scanners.

=head4 # Pages

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

=head4 Source document

Selects between single sided or double sides pages.

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

=head4 Side to scan

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

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

=over

=item 1. Select:

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

Double sided

Facing side

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

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

=item 4. Select:

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

Double sided

Reverse side

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

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

=back

=head4 Device-dependent options

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

=over

=item Page size.

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

=item Resolution (in dpi)

=item Batch-scan

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

=item Wait-for-button/Button-wait

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

=item Source

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

=back

=head3 Save PDF

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

=head4 Metadata

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

The metadata are completely optional.

=head3 Save image

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

=head3 Save DjVu

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

=head3 Email as PDF

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

=head2 Edit

=head3 Delete

Deletes the selected page.

=head3 Renumber

Renumbers the pages from 1..n.

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

=head3 Select All

Selects all pages.

=head3 Frontend

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

=head2 View

=head3 Zoom 100%

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

=head3 Zoom to fit

Scales the view such that all the page is visible.

=head3 Zoom in

=head3 Zoom out

=head3 Rotate 90 clockwise

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

=head3 Rotate 180

=head3 Rotate 90 anticlockwise

=head2 Tools

=head3 Threshold

Changes all pixels darker than the given value to black; all others become white.

=head3 unpaper

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

=head3 OCR (Optical Character Recognition)

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

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

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

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

=head1 FAQs

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

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

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

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

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

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

=head2 Why is option xyz ghosted out?

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

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

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

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

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

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

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

=head1 Roadmap

=over

=item v1.0.0

=over

=item Scan profiles.

=back

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

=over

=item Correct positioning and size of OCR output behind scan -
      as soon as tesseract supports outputting the information in a sensible
      manner.

=item Crop function (As soon as I have finished the Perl bindings for gtkimageview).

=item Print (using Net::Cups).

=item Copier.

=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

