#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process
# Copyright (C) 2006 -- 2009  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
#    fix Wide character in print at /usr/share/perl5/Config/General.pm line 1118. message.
#     Report bug and switch to storable?
#    add find text in ocr buffer - search through all buffers
#    scanning doesn't work after fork()
#    window too tall after options have been found
#    add profiles
#    support multiple value options
#    add units
#    Move the English comment in the .desktop file to the template,
#     and write the translations on the fly back to the desktop file.
#    look at scan insert before/after page/start/end
#    Remember sort on FileChooser
#    pass array reference to combobox_from_array, and optionally the default.
#    Investigate using Gtk2::FileChooser instead of Gtk2::FileChooserDialog
#     and putting save options on same dialog.
#    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
#    note sane-backend and wipe scan settings if it changes
#    reenable multiple scans from flatbed + delay and warning
#    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?)
#    cope with or prevent pages being deleted during scan/unpaper/ocr
#    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
#    add run script option
#    Add status line at bottom with messages like "filename 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.
#    As soon as I have cups-config --version >= 1.2.2, move print routines to
#     Net::Cups
#
# Release procedure:
#    Use perltidy immediately before release so as not to affect any patches
#    in between, and then consistently before each commit afterwards.
# 0. Test scan in lineart, greyscale and colour.
# 1. New screendump required? Print screen creates screenshot.png in Desktop.
#    Download new translations
#    Update translators in credits
#    Check a translation with LANGUAGE=de bin/gscan2pdf --debug
#    Check $version
#    Make appropriate updates to debian/changelog
# 2. perl Makefile.PL
#    make rpmdist debdist
#    test dist sudo dpkg -i /tmp/gscan2pdf/gscan2pdf-x.x.x_all.deb
# 3. git status
#    git tag vx.x.x
#    git push --tags ssh://ra28145@gscan2pdf.git.sourceforge.net/gitroot/gscan2pdf master
# 4. make remote-html 
# 5. Upload to sourceforge (https://frs.sourceforge.net/webupload) and release files
# 6. Freshmeat
# 7. Launchpad (https://launchpad.net/gscan2pdf), upload .pot if necessary
# 8. http://www.gtkfiles.org/app.php/gscan2pdf
# 9. make ppa
#    name the release -0~ppa1, debuild -S -sa, dput gscan2pdf-ppa .changes
#    https://launchpad.net/~jeffreyratcliffe/+archive
#    gscan2pdf-announce, gnome-announce-list@gnome.org, sane-devel
#    ftp-master (check contents with dpkg-deb --contents)

use warnings;
use strict;
use forks 0.33;
use forks::shared;
use Thread::Semaphore;
use Gscan2pdf;
use Gtk2 -init;
use Gtk2::ImageView;
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 2.40;
use Text::ParseWords;
use Archive::Tar;                    # For session files
use Sane;
use PDF::API2;
use Getopt::Long;
use Set::IntSpan 1.10;               # For page numbering issues. 1.10 required for size method
use Storable qw(freeze thaw);        # to pass big data structures between threads

# 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 $prog_name = 'gscan2pdf';
my $version = '0.9.29';

my $tolerance = 1;
my $buffer_size = (32 * 1024);	# default size

# 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/$prog_name.mo
# or whatever is set by $d->dir([NEWDIR]);
my $d = Locale::gettext->domain($prog_name);
my $d_sane = Locale::gettext->domain('sane-backends');

my $debug = FALSE;
my ($test, $help, @device, @model, $locale, @device_list, $test_image);
my @args = ('device=s' => \@device,
            'test=s%' => \$test,
            'test-image=s' => \$test_image,
            'locale=s' => \$locale,
            'help' => \$help,
            'debug' => \$debug,
            'version' => sub {warn "$prog_name $version\n";exit 0});
exit 1 if (! GetOptions (@args));
$Sane::DEBUG = $debug;

if (defined $help) {
 system("perldoc $0") == 0 or warn $d->get("Error displaying help\n");
 exit;
}

warn "$prog_name $version\n" if (defined $debug);

if (defined $locale) {
 if ($locale !~ /^\//) {
  $d->dir(getcwd."/$locale");
 }
 else {
  $d->dir($locale);
 }
}

@model = @device if (@device);
for (@device) {
 push @device_list, {name => $_, label => $_};
}

# 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'}/.$prog_name";
system("touch $rc") if (! -r $rc);
my ($conf, %SETTING);
if (eval {$conf = Config::General->new(
 -ConfigFile => $rc, -SplitPolicy => 'equalsign',
)}) {
 %SETTING = $conf->getall;
}
else {
 print $d->get("Error: unable to load settings.\nBacking up settings\nReverting to defaults"), "\n";
 move($rc, "$rc.old");
}

my %default_settings = (
 'window_width'      => 800,
 'window_height'     => 600,
 'window_maximize'   => TRUE,
 'thumb panel'       => 100,
 'Page range'        => 'all',
 'layout'            => 'single',
 'downsample'        => FALSE,
 'downsample dpi'    => 150,
 'threshold tool'    => 80,
 'unsharp radius'    => 0,
 'unsharp sigma'     => 1,
 'unsharp amount'    => 1,
 'unsharp threshold' => 0.05,
 'cache options'     => TRUE,
 'restore window'    => TRUE,
 'startup warning'   => FALSE,
 'date offset'       => 0,
 'pdf compression'   => 'auto',
 'quality'           => 75,
 'pages to scan'     => 1,
 'unpaper on scan'   => TRUE,
 'OCR on scan'       => TRUE,
 'frontend'          => 'libsane-perl',
 'rotate facing'     => 0,
 'rotate reverse'    => 0,
 'scan prefix'       => '',
 'Blank threshold'   => 0.005, # Blank page standard deviation threshold
 'Dark threshold'    => 0.12,  # Dark page mean threshold
 'OCR output'        => 'replace', # When a page is re-OCRed, replace old text with new text
 '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,
  },
 },
);
delete $SETTING{frontend}
 if (defined $SETTING{frontend} and
  ($SETTING{frontend} eq 'scanimage.pl' or $SETTING{frontend} eq 'scanadf.pl'));
for (keys(%default_settings)) {
 $SETTING{$_} = $default_settings{$_} unless (defined $SETTING{$_});
}

# Remove invalid paper sizes
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;
   }
  }
 }
}

# Delete the options cache if there is a new version of SANE
if ((defined($SETTING{'SANE version'})
     and $SETTING{'SANE version'} ne join('.',Sane->get_version))
    or (defined($SETTING{'libsane-perl version'})
     and $SETTING{'libsane-perl version'} ne $Sane::VERSION)) {
 delete $SETTING{cache} if (defined $SETTING{cache});
}
$SETTING{'SANE version'} = join('.',Sane->get_version);
$SETTING{'libsane-perl version'} = $Sane::VERSION;

# Set up test mode and make sure file has absolute path and is readable
if (keys %{$test}) {
 $SETTING{frontend} = 'scanimage-perl';
 for my $file (keys %{$test}) {
  my $device = $test->{$file};
  delete $test->{$file};

# Find a way of emulating the nonsense \n that some people seem to get
  $device =~ s/\\n/\n/g;
  print "'$file','$device'\n";
  $file = getcwd."/$file" if ($file !~ /^\//);
  if (! -r $file) {
   warn sprintf($d->get("Error: cannot read file: %s\n"), $file);
   exit 1;
  }
  push @{$test->{file}}, $file;
  $test->{output} = '' if (! defined($test->{output}));
  $test->{output} .= "'$#{$test->{file}}','$device','".basename($file)."'\n";
 }
}
# GetOptions leaves $test as a reference to an empty hash.
else {
 undef $test;
}

if (defined $test_image) {
 if (-r $test_image) {
  print "Using test image $test_image\n" if $debug;
 }
 else {
  warn sprintf($d->get("Error: cannot read file: %s\n"), $test_image);
  exit 1;
 }
}

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";
 print "Using GtkImageView version ", Gtk2::ImageView->library_version, "\n";
 print "Using Gtk2::ImageView version ", $Gtk2::ImageView::VERSION, "\n";
 print "Using PDF::API2 version ", $PDF::API2::Version::CVersion{vShort}, "\n";

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

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


# Initialise threads
my (%shash, %thread);
#share(%shash); #will work only for first level keys 
my $numthreads = 1;
my $sane_thread = 1; # Always use thread 1 for SANE
foreach (1..$numthreads) {
 $shash{$_}{semaphore} = Thread::Semaphore->new(0);
 share ($shash{$_}{semaphore});
 share ($shash{$_}{device});
 share ($shash{$_}{status});
 share ($shash{$_}{go});
 share ($shash{$_}{process});
 share ($shash{$_}{progress});
 share ($shash{$_}{data});
 share ($shash{$_}{die});
 $thread{$_} = threads->new(sub {
  my $device; # variable local to thread for storing sane device object
  my $fh;     # variable local to thread for storing file handle
  while (TRUE) {
   $shash{$_}{semaphore}->down;
   last if $shash{$_}{die};
   lock($shash{$_}{process});
   eval $shash{$_}{process}; ## no critic (ProhibitStringyEval)
   warn("Error running process: $@") if ($@);
   print "$shash{$_}{process}\n" if ($@);
  }
 });
}


# 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" ],
 [ 'selection',	"$path/stock-selection-all-16.png" ],
);

# Define application-wide variables here so that they can be referenced
# in the menu callbacks
my ($windowi, $windowe, $windows, $windows2, $windowh, $windowo, $windowrn,
    $windowu, $slist, $vboxd, $labeld, $combobd, $combobp, $vboxm, @undo_buffer,
    @redo_buffer, @undo_selection, @redo_selection, %dependencies,
    @ocr_stack, $running_ocr, %helperTag, @unpaper_stack, $running_unpaper,
    $scanning, @ocr_engine, $bscanall, $bscannum, @clipboard, $windowd,
    $windowr, @rotate_queue, $rotating, $view, $frames, $duplex, $windowp,
    @analyze_queue, $analyzing, $batch_scan, $gui_updating,

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

# Notebook on scan dialog
    $notebook,

# Scan button on scan dialog
    $sbutton,

# List of PageRange widgets
    @prlist);

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 ],
 [ 'Open', 'gtk-revert-to-saved', $d->get('_Open'), '<control>o', $d->get('Open gscan2pdf session file'), \&open_dialog ],
 [ 'Import', 'gtk-open', $d->get('_Import'), '<control>i', $d->get('Import image file(s)'), \&import_dialog ],
 [ 'Scan', 'scanner', $d->get('S_can'), undef, $d->get('Scan document'), sub { $SETTING{frontend} eq 'libsane-perl' ? scan_dialog2() : scan_dialog() } ],
 [ 'Save', 'gtk-save', $d->get('Save'), '<control>s', $d->get('Save'), \&save_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'), \&renumber_dialog ],
 [ 'Select', undef, $d->get('_Select') ],
 [ 'Select All', 'gtk-select-all', $d->get('_All'), '<control>a', $d->get('Select all pages'), \&select_all ],
 [ 'Select Odd', undef, $d->get('_Odd'), '<control>1', $d->get('Select all odd-numbered pages'), sub { select_odd_even(0); } ],
 [ 'Select Even', undef, $d->get('_Even'), '<control>2', $d->get('Select all evenly-numbered pages'), sub { select_odd_even(1); } ],
 [ 'Select Blank', 'gtk-select-blank', $d->get('_Blank'), '<control>b', $d->get('Select pages with low standard deviation'), \&analyze_select_blank ],
 [ 'Select Dark', 'gtk-select-blank', $d->get('_Dark'), '<control>d', $d->get('Select dark pages'), \&analyze_select_dark ],
 [ 'Select Modified', 'gtk-select-modified', $d->get('Select _Modified'), '<control>m', $d->get('Select modified pages since last OCR'), \&select_modified_since_ocr ],
 [ 'Properties', 'gtk-properties', $d->get('Propert_ies'), undef, $d->get('Edit image properties'), \&properties ],
 [ 'Preferences', 'gtk-preferences', $d->get('Prefere_nces'), undef, $d->get('Edit preferences'), \&preferences ],

 # View menu
 [ 'View', undef, $d->get('_View') ],
 [ 'Zoom 100', 'gtk-zoom-100', $d->get('Zoom _100%'), undef, $d->get('Zoom to 100%'), sub { $view->set_zoom(1.0); } ],
 [ 'Zoom to fit', 'gtk-zoom-fit', $d->get('Zoom to _fit'), undef, $d->get('Zoom to fit'), sub { $view->set_fitting(TRUE); } ],
 [ 'Zoom in', 'gtk-zoom-in', $d->get('Zoom _in'), 'plus', $d->get('Zoom in'), sub { $view->zoom_in; } ],
 [ 'Zoom out', 'gtk-zoom-out', $d->get('Zoom _out'), 'minus', $d->get('Zoom out'), sub { $view->zoom_out; } ],
 [ 'Rotate 90', 'rotate90', $d->get('Rotate 90 clockwise'), '<control><shift>R', $d->get('Rotate 90 clockwise'), sub { rotate_selected(90); } ],
 [ 'Rotate 180', 'rotate180', $d->get('Rotate 180'), '<control><shift>F', $d->get('Rotate 180'), sub { rotate_selected(180); } ],
 [ 'Rotate 270', 'rotate270', $d->get('Rotate 90 anticlockwise'), '<control><shift>C', $d->get('Rotate 90 anticlockwise'), sub { rotate_selected(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 ],
 [ 'Negate', undef, $d->get('_Negate'), undef, $d->get('Converts black to white and vice versa'), \&negate ],
 [ 'Unsharp', undef, $d->get('_Unsharp Mask'), undef, $d->get('Apply an unsharp mask'), \&unsharp ],
 [ 'Crop', 'GTK_STOCK_LEAVE_FULLSCREEN', $d->get('_Crop'), undef, $d->get('Crop pages'), \&crop ],
 [ 'GIMP', undef, $d->get('_GIMP'), undef, $d->get('Edit current page with GIMP'), \&gimp ],
 [ 'unpaper', undef, $d->get('_Clean up'), undef, $d->get('Clean up scanned images 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 @image_tools = (
 [ 'DraggerTool', 'gtk-refresh', $d->get('_Drag'), undef, $d->get('Use the hand tool'), 10 ],
 [ 'SelectorTool', 'selection', $d->get('_Select'), undef, $d->get('Use the rectangular selection tool'), 20 ],
 [ 'PainterTool', 'gtk-media-play', $d->get('_Paint'), undef, $d->get('Use the painter tool'), 30 ]
);


# Declare the XML structure
my $uimanager;
my $ui = "<ui>
 <menubar name='MenuBar'>
  <menu action='File'> 
   <menuitem action='New'/>
   <menuitem action='Open'/>
   <menuitem action='Import'/>
   <menuitem action='Scan'/>
   <menuitem action='Save'/>
   <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'/>
   <separator/>
   <menuitem action='Renumber'/>
   <menu action='Select'> 
    <menuitem action='Select All'/>
    <menuitem action='Select Odd'/>
    <menuitem action='Select Even'/>
    <menuitem action='Select Blank'/>
    <menuitem action='Select Dark'/>
    <menuitem action='Select Modified'/>
   </menu>
   <separator/>
   <menuitem action='Properties'/>
   <separator/>
   <menuitem action='Preferences'/>
  </menu>
  <menu action='View'> 
   <menuitem action = 'DraggerTool'/>
   <menuitem action = 'SelectorTool'/>
   <separator/>
   <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'/>
   <menuitem action='Negate'/>
   <menuitem action='Unsharp'/>
   <menuitem action='Crop'/>
   <menuitem action='GIMP'/>
   <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='Open'/>
  <toolitem action='Import'/>
  <toolitem action='Scan'/>
  <toolitem action='Save'/>
  <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'/>
  <separator/>
  <toolitem action='Renumber'/>
  <toolitem action='Select All'/>
  <separator/>
  <toolitem action = 'DraggerTool'/>
  <toolitem action = 'SelectorTool'/>
  <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 = 'DraggerTool'/>
  <menuitem action = 'SelectorTool'/>
  <separator/>
  <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'/>
  <separator/>
  <menuitem action='Properties'/>
 </popup>
 <popup name='Thumb_Popup'>
  <menuitem action='Save'/>
  <menuitem action='Email as PDF'/>
  <separator/>
  <menuitem action='Renumber'/>
  <menuitem action='Select All'/>
  <menuitem action='Select Odd'/>
  <menuitem action='Select Even'/>
  <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'/>
  <separator/>
  <menuitem action='Properties'/>
 </popup>
</ui>";

# Create the window
my $window = Gtk2::Window -> new;
$window -> set_title ( "$prog_name 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 ($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'});
}

eval { $window -> set_default_icon_from_file ("$path/gscan2pdf.svg"); };
warn("Unable to load icon `$path/gscan2pdf.svg': $@") if ($@);

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;
# resize = FALSE to stop the panel expanding on being resized (Debian #507032)
$hpaned -> pack1($scwin_thumbs, FALSE, TRUE);
$scwin_thumbs -> set_policy('automatic', 'automatic');
$scwin_thumbs -> set_shadow_type('etched-in');

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

# Set up a SimpleList
$slist = Gtk2::Ex::Simple::List -> new('#' => 'int',
                                       $d->get('Thumbnails') => 'pixbuf',
                                       'Filename' => 'hstring',
                                       'Buffer' => 'hstring',
                                       'Resolution' => 'hstring',
                                       'Saved' => 'hstring',
                                       'Mean' => 'hstring', #6 store image stats
                                       'StdDev' => 'hstring', #store image stats
                                       'Dirty' => 'hstring', #timestamp image when changed (which may affect OCR)
                                       'FlagOCR' => 'hstring', #update OCR automatically on this page
                                       'OCRtime' => 'hstring', #10 timestamp of last OCR
                                       'AnalyzeTime' => 'hstring', #timestamp of last analyze
                                       );
$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;
  $suffix = $1 if ($info[2] =~ /(\.\w*)$/);
  my (undef, $new) = tempfile(DIR => $SETTING{session}, 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 -> {row_changed_signal});

 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 -> {row_changed_signal});

# 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 -> {row_changed_signal} = $slist -> get_model ->
 signal_connect('row-changed' => sub {
  $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});

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

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

# ImageView for detail view
$view = Gtk2::ImageView->new;
my $dragger = Gtk2::ImageView::Tool::Dragger->new ($view);
my $selector = Gtk2::ImageView::Tool::Selector->new ($view);
my $painter = Gtk2::ImageView::Tool::Painter->new ($view);
$vpaned -> pack1 ($view, TRUE, TRUE);

$view->signal_connect(button_press_event => sub {
 my ($widget, $event) = @_;
 handle_clicks($widget, $event);
});
$view->signal_connect(button_release_event => \&handle_clicks);

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

# Set up call back for list selection to update detail view
$slist -> {selection_changed_signal} = $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);
  my $sel = $selector->get_selection;
  display_image($slist->{data}[$page[0]][2]);
  $selector->set_selection ($sel) if (defined $sel);

# 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 {
  $view->set_pixbuf(undef);
  $textview -> set_sensitive(FALSE);
  $textbuffer -> set_text('');
 }

# If no pages are selected, but some exist, PageRange widgets default to all
 if ($#page == -1 and $#{$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') },
            'gain'            => { string => $d->get('Gain') },
            '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') },
            'page-width'      => { string => $d->get('Page Width') },
            'page-height'     => { string => $d->get('Page Height') },
            'overscan-top'    => { string => $d->get('Top Overscan') },
            'overscan-bottom' => { string => $d->get('Bottom Overscan') },
            'adf_mode'        => { string => $d->get('ADF Mode'),
                                   values => {
                                             'Simplex' => $d->get('Simplex'),
                                             'Duplex'  => $d->get('Duplex'),
                                             },
                                 },
            'adf-mode'        => { string => $d->get('ADF Mode'),
                                   values => {
                                             'Simplex' => $d->get('Simplex'),
                                             'Duplex'  => $d->get('Duplex'),
                                             },
                                 },
          );

# 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',
 },
 'output-pages' => {
  type => 'SpinButton',
  string => $d->get('# Output pages'),
  tooltip => $d->get('Number of pages to output.'),
  min => 1,
  max => 2,
  step => 1,
  default => 1,
 },
 '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();

# Look for crashed session
if (defined $SETTING{session}) {
 if (-r "$SETTING{session}/session") {
  open_session();
 }
 else {
  warn "gscan2pdf detected a crashed session, but was unable to restore it as it has been deleted.\n";
  delete $SETTING{session};
 }
}

# Create temporary directory if necessary
unless (defined $SETTING{session}) {
 if (defined($SETTING{TMPDIR}) and $SETTING{TMPDIR} ne '') {
  mkdir($SETTING{TMPDIR}) if (not -d $SETTING{TMPDIR});
  if (! eval {$SETTING{session} = tempdir(DIR => $SETTING{TMPDIR})}) {
   $SETTING{session} = tempdir;
   warn sprintf($d->get("Warning: unable to use %s for temporary storage. Defaulting to %s instead.\n"),
                                  $SETTING{TMPDIR}, dirname($SETTING{session}));
  }
 }
 else {
  $SETTING{session} = tempdir;
 }
 print "Using $SETTING{session} for temporary files\n" if $debug;

# Save session data to settings
 $conf->save_file($rc, \%SETTING);
}

$window -> show_all;



### 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);
 $actions_basic->add_radio_actions (\@image_tools,
                                       10,
                                       \&change_image_tool_cb);

# Add the actiongroup to the uimanager  
 $uimanager->insert_action_group($actions_basic, 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{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 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});

# Ghost save image item if imagemagick not available
 my $msg = '';
 $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') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Email as PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Save') -> set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Email as PDF') -> set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Save') -> 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/Negate') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/Unsharp') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/Crop') -> set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/GIMP') -> 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 and unpaper support 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 (not $dependencies{gocr} and not $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 ($SETTING{'startup warning'} and $msg ne '') {
  my $dialog = Gtk2::Dialog -> new ($d->get('Warning: missing packages'),
                                    $window, 'modal',
                                    '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/DraggerTool',
                 '/MenuBar/View/SelectorTool',
                 '/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/Negate',
                 '/MenuBar/Tools/Unsharp',
                 '/MenuBar/Tools/Crop',
                 '/MenuBar/Tools/GIMP',
                 '/MenuBar/Tools/unpaper',
                 '/MenuBar/Tools/OCR',

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

                 '/Detail_Popup/DraggerTool',
                 '/Detail_Popup/SelectorTool',
                 '/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{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} and $dependencies{libtiff}) {
    $uimanager->get_widget('/MenuBar/File/Save') -> set_sensitive(TRUE);
    $uimanager->get_widget('/ToolBar/Save') -> set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Save') -> set_sensitive(TRUE);
   }

   $dependencies{pages} = $#{$slist -> {data}};
  }
 }
 else {
  if ($dependencies{pages} > -1) {
   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} and $dependencies{libtiff}) {
    $uimanager->get_widget('/MenuBar/File/Save') -> set_sensitive(FALSE);
    $uimanager->get_widget('/ToolBar/Save') -> set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Save') -> set_sensitive(FALSE);
   }
   $windowi->hide if defined $windowi;

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

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


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

 my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file ($filename);
 $view->set_pixbuf($pixbuf);
 return;
}


# 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); };
  warn sprintf($d->get("Information: got %s on second attempt\n"), $filename)
   unless ($@);
 }

 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();
 return;
}


sub get_resolution {
 my $image = shift;
 my $resolution;
 my $format = $image->Get('format');

 # Imagemagick always reports PNMs as 72dpi
 if ($format ne 'Portable anymap') {
  $resolution = $image->Get('x-resolution');
  return $resolution if ($resolution);

  $resolution = $image->Get('y-resolution');
  return $resolution if ($resolution);
 }

# Guess the resolution from the shape
 my $height = $image->Get('height');
 my $width = $image->Get('width');
 my $ratio = $height/$width;
 $ratio = 1/$ratio if ($ratio < 1);
 $resolution = 72;
 for (keys %{$SETTING{Paper}}) {
  if ($SETTING{Paper}{$_}{x} > 0
      and abs($ratio - $SETTING{Paper}{$_}{y}/$SETTING{Paper}{$_}{x}) < 0.02) {
   $resolution = int((($height > $width) ? $height : $width)/$SETTING{Paper}{$_}{y}*25.4 + 0.5);
  }
 }
 return $resolution;
}


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

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


# Throw up file selector and open session file

sub open_dialog {

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

 my $file_chooser = Gtk2::FileChooserDialog -> new(
                                      $d->get('Open session file'),
                                      $window, 'open',
                                      'gtk-cancel' => 'cancel',
                                      'gtk-ok' => 'ok');
 $file_chooser -> set_default_response('ok');

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

# cd back to tempdir to import
  chdir $SETTING{session};

# Update undo/redo buffers
  take_snapshot();

  my $filename = $file_chooser -> get_filename;
  $file_chooser -> destroy;

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

  my $dialog = Gtk2::Dialog -> new ($d->get('Opening')."...", $window,
                                    'modal',
                                    '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;

  open_session($filename);

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

#  my ($child, $parent) = open_socketpair();
#  my $pid = start_process(sub {
#  });

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

#   my $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 (not 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 $SETTING{session};
 return;
}


sub open_session {
 my ($filename) = @_;
 print "Restoring session in $SETTING{session}\n" if $debug;
 if (defined $filename) {
  new(); # clear the page list
  my $tar = Archive::Tar->new($filename, TRUE);
  my @filenamelist = $tar->list_files;
  $tar->extract;
 }
 my $sessionfile = "$SETTING{session}/session";
 my $session = Config::General->new(-ConfigFile => $sessionfile);
 my %session = $session->getall;

# Block the row-changed signal whilst adding the scan (row) and sorting it.
 $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
 my @selection = $session{selection};
 delete $session{selection};
 foreach my $page (sort {$a <=> $b} (keys(%session))) {
  $session{$page}{filename} = File::Spec->catfile($SETTING{session},
                                            basename($session{$page}{filename}))
   if (dirname($session{$page}{filename}) ne $SETTING{session});
  push @{ $slist->{data} }, [ $page,
                              get_pixbuf($session{$page}{filename}, $heightt, $widtht),
                              $session{$page}{filename},
                              $session{$page}{buffer},
                              $session{$page}{resolution} ];
 }
 $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
 $slist -> select(@selection);
 return;
}


# Throw up file selector and import selected file

sub import_dialog {

# 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 $SETTING{session};

# Update undo/redo buffers
  take_snapshot();

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

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

  my $dialog = Gtk2::Dialog -> new ($d->get('Importing')."...", $window,
                                    'modal',
                                    '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 ($child, $parent) = open_socketpair();
  my $pid = start_process(sub {

   my $j = 0;
   foreach (@filename) {
    import_file($_, $j, $#filename+1, $parent);
   }
   send($parent, 2, 0);
  });

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($child, $line, 1000, 0);
    if (defined($line) and $line ne '') {
     my ($fraction, $filename, $resolution, @text) = split ' ', $line;
     if ($fraction == -1) {
      show_message_dialog($window, 'error', 'close', join(' ', @text));
     }
     elsif ($fraction == -2) {
      my $pages = $resolution;
      $dialog -> hide;
      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, $pages, 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, $pages, 1);
      $spinbuttonl->set_value($pages);
      $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($child, "$first $last", 0);
       $dialog -> show;
      });

# 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($child, '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 (not 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 $SETTING{session};
 return;
}


# import file

sub import_file {
 my ($filename, $j, $n, $parent) = @_;

 warn "Importing $filename\n" if ($debug);

# Check if djvu
 my $buffer;
 open my $fh, '<', $filename or
  send($parent, sprintf("%f %s %i ", -1, 0, 0).
                       sprintf($d->get("Can't open %s: %s"), $filename, $!), 0);
 binmode $fh;
 read ($fh, $buffer, 8);
 close $fh;
 if ($buffer eq 'AT&TFORM') {

# Dig out the number of pages
  my $cmd = "djvudump \"$filename\"";
  warn "$cmd\n" if ($debug);
  my $info = `$cmd`;
  print $info if ($debug);

  my $first = 1;
  my $last = 1;
  $last = $1 if ($info =~ /\s(\d+)\s+page/);

# Dig out and the resolution of each page
  my @dpi;
  while ($info =~ /\s(\d+)\s+dpi/) {
   push @dpi, $1;
   warn "Page $#dpi is $dpi[$#dpi] dpi\n" if ($debug);
   $info = substr($info, index($info, " dpi")+4, length($info));  
  }
  if ($last != @dpi) {
   send($parent, sprintf("%f %s %i ", -1, 0, 0)
   .$d->get('Unknown DjVu file structure. Please contact the author.'), 0);
  }
  else {
   if ($last > 1) {
    send($parent, "-2 0 $last", 0);

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

# Extract images from DjVu
   if ($last >= $first and $first > 0) {
    for (my $i = $first; $i <= $last; $i++) {
     my (undef, $tif) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
     my $cmd = "ddjvu -format=tiff -page=$i \"$filename\" $tif";
     warn "$cmd\n" if ($debug);
     system($cmd);
     send($parent, 
      sprintf("%f %s %i ", ($i/$last+$j)/$n, $tif, $dpi[$i-1])
                    .sprintf($d->get("Importing image %i of %i"), $i, $last), 0);
    }
   }
   else {
    send($parent, '2 0 0', 0);
   }
  }
  ++$j;
  return;
 }

# 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($parent, 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\"`;
  print $info if ($debug);
  if ($info =~ /Pages:\s+(\d+)/) {
   my $last = $1;
   print "$last pages\n" if ($debug);
   my $first = 1;
   if ($last > 1) {
    send($parent, "-2 0 $last", 0);

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

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

# Import each image
    my @images = glob('x-???.???');
    my $i = 0;
    foreach (@images) {
     my ($filename, $resolution) = prepare_import($_, 'Portable anymap', undef, TRUE);
     send($parent, 
      sprintf("%f %s %i ", (++$i/($#images+1)+$j)/$n, $filename, $resolution)
      .sprintf($d->get("Importing image %i of %i"), $i, $#images+1), 0);
    }
   }
   else {
    send($parent, '2 0 0', 0);
   }
  }
  else {
   send($parent, '-1 0 0'.$d->get('Error extracting PDF infomation'), 0);
  }
 }
 elsif ($format eq 'Tagged Image File Format') {
  send($parent, '0 0 0'.$d->get('Extracting TIFF infomation'), 0);
  my $cmd = "tiffinfo \"$filename\"";
  warn "$cmd\n" if ($debug);
  my $info = `$cmd`;
  print $info if ($debug);

# Count number of pages and their resolutions
  my @dpi;
  while ($info =~ /Resolution: (\d*)/) {
   push @dpi, $1;
   $info = substr($info, index($info,'Resolution')+10, length($info));
  }
  my $last = @dpi;
  print "$last pages\n" if ($debug);
  my $first = 1;
  if ($last > 1) {
   send($parent, "-2 0 $last", 0);

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

# Split the tiff into its pages and import them individually
  if ($last >= $first and $first > 0) {
   for (my $i = $first-1; $i < $last; $i++) {
    my (undef, $tif) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
    my $cmd = "tiffcp \"$filename\",$i $tif";
    warn "$cmd\n" if ($debug);
    system($cmd);
    send($parent, 
     sprintf("%f %s %i ", (($i-$first+1)/($last-$first+1)+$j)/$n, $tif, $dpi[$i])
     .sprintf($d->get("Importing image %i of %i"), $i-$first, $last-$first+1), 0);
   }
  }
  else {
   send($parent, '2 0 0', 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($parent, sprintf("%f %s %i ", $j/$n, $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($parent, sprintf("%f %s %i ", $j/$n, $filename, $resolution)
   .sprintf($d->get("Importing %s"), $format), 0);
 }

 ++$j;
 return;
}


# 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 -> pack_start ($frame, FALSE, FALSE, 0);

 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;
 });
 $frame->add ($pr);
 push @prlist, $pr;
 return;
}


# 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 '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 pages depending on which radiobutton is active

sub get_page_index {
 if ($SETTING{'Page range'} eq 'all') {
  return 0..$#{$slist -> {data}};
 }
 elsif ($SETTING{'Page range'} eq 'selected') {
  return $slist -> get_selected_indices;
 }
 return;
}


# 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);
 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($windowi, $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;
 return;
}


sub get_PDF_options {
 my %h;
 $h{'Author'} = $SETTING{'author'} if defined $SETTING{'author'};
 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'} = "$prog_name 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) = @_;

 my $dialog = Gtk2::Dialog -> new ($d->get('Saving PDF')."...", $window,
                                   'modal',
                                   '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 ($child, $parent) = open_socketpair();
 my $pid = start_process(sub {
  my $page = 0;

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

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

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

# Automatic mode
   my $depth;
   my $compression;
   my $type;
   if ($SETTING{'pdf compression'} eq 'auto') {
    $depth = $image->Get('depth');
    print "Depth of $filename is $depth\n" if ($debug);
    if ($depth == 1) {
     $compression = 'lzw';
    }
    else {
     $type = $image->Get('type');
     print "Type of $filename is $type\n" if ($debug);
     if ($type =~ /TrueColor/) {
      $compression = 'jpg';
     }
     else {
      $compression = 'png';
     }
    }
    print "Selecting $compression compression\n" if ($debug);
   }
   else {
    $compression = $SETTING{'pdf compression'};
   }

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

    $depth = $image->Get('depth') if (! defined $depth);
    if ($SETTING{'downsample'}) {
     $w = $w/$resolution*$SETTING{'downsample dpi'};
     $h = $h/$resolution*$SETTING{'downsample dpi'};
     $resolution = $SETTING{'downsample dpi'};
     print "Resizing $filename to $w x $h\n" if ($debug);
     $x = $image->Resize(width => $w, height => $h);
     warn "$x" if "$x";
    }
    $x = $image->Set(quality => $SETTING{quality})
     if ($compression eq 'jpg');
    warn "$x" if "$x";

    if (($compression !~ /(jpg|png)/ and $format ne 'tif')
        or ($compression =~ /(jpg|png)/) or $SETTING{'downsample'}) {
# depth required because resize otherwise increases depth to maintain information
     print "Writing temporary image $filename with depth $depth\n" if ($debug);
     $x = $image->Write(filename => $filename, depth => $depth);
     warn "$x" if "$x";
     $format = $1 if ($filename =~ /\.(\w*)$/);
    }

    if ($compression !~ /(jpg|png)/) {
     my (undef, $filename2) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
     my $cmd = "tiffcp -c $compression $filename $filename2";
     print "$cmd\n" if $debug;
     my $status = system("$cmd 2>$SETTING{session}/tiffcp.stdout");
     if ($status != 0) {
      my $output = slurp("$SETTING{session}/tiffcp.stdout");
      print $output if $debug;
      send($parent, '-1'.sprintf($d->get("Error compressing image: %s"), $output), 0);
     }
     $filename = $filename2;
    }
   }

   print "Defining page at ", $w*72/$resolution, " x ", $h*72/$resolution, "\n"
    if $debug;
   my $page = $pdf->page;
   $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;
   my $msg;
   if ($format eq 'png') {
    eval {$imgobj = $pdf->image_png($filename)};
    $msg = "$@";
   }
   elsif ($format eq 'jpg') {
    eval {$imgobj = $pdf->image_jpeg($filename)};
    $msg = "$@";
   }
   elsif ($format eq 'pnm') {
    eval {$imgobj = $pdf->image_pnm($filename)};
    $msg = "$@";
   }
   elsif ($format eq 'gif') {
    eval {$imgobj = $pdf->image_gif($filename)};
    $msg = "$@";
   }
   elsif ($format eq 'tif') {
    eval {$imgobj = $pdf->image_tiff($filename)};
    $msg = "$@";
   }
   else {
    $msg = "Error embedding file $filename in $format format to PDF\n";
   }
   if ($msg) {
    warn $msg if $debug;
    send($parent, '-1'.sprintf($d->get("Error embedding file image in %s format to PDF: %s"), $format, $msg), 0);
   }
   else {
    eval {$gfx->image($imgobj,0,0,72/$resolution)};
    if ($@) {
     warn $@ if $debug;
     send($parent, '-1'.sprintf($d->get("Error embedding file image in %s format to PDF: %s"), $format, $@), 0);
    }
    else {
     print "Adding $filename at $resolution dpi\n" if $debug;
    }
   }
  }
  send($parent, '1'.$d->get('Closing PDF'), 0);
  $pdf->save;
  $pdf->end;
  send($parent, '2', 0);
 });

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($child, $line, 1000, 0);
   if ($line =~ /(-?\d*\.?\d*)(.*)/) {
    my $fraction=$1;
    my $text=$2;
    if ($fraction == -1) {
     show_message_dialog($window, 'error', 'close', $text);
    }
    elsif ($fraction > 1) {
     $dialog -> destroy;
     mark_pages(@pagelist);
     return FALSE;  # uninstall
    }
    else {
     $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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   update_uimanager();
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 return;
}


# 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'),
                                                    $windowi, 'save',
                                                    'gtk-cancel' => 'cancel',
                                                    'gtk-save' => 'ok');
 $file_chooser -> set_current_name ($SETTING{'title'})
  if (defined($SETTING{'title'}) and $SETTING{'title'} ne '');
 $file_chooser -> set_default_response('ok');
 $file_chooser -> set_do_overwrite_confirmation (TRUE);
 $file_chooser -> signal_connect (response => sub {
  my ($dialog, $response) = @_;
  if ($response eq 'ok') {
   my $filename = $file_chooser -> get_filename;
   if ($filename !~ /\.pdf$/i) {
    $filename = $filename.'.pdf';
    if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
     $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
     Glib::Idle->add (sub { $file_chooser->response('ok'); });
     return;
    }
   }

# Check that the file can be written
   if (! -w dirname($filename)) {
    show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
    return;
   }

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

# Create the PDF
   create_PDF($filename);
  
   $windowi -> hide if defined $windowi;
  }
  $file_chooser -> destroy;
 });
 $file_chooser -> show;

# cd back to tempdir
 chdir $SETTING{session};
 return;
}


# 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);
 $spinbutton->set_value($SETTING{'quality'});
 $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 = (
  [ 'auto', $d->get('Automatic'), $d->get('Let gscan2pdf which type of compression to use.') ],
  [ '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(@compression);
 $combob -> signal_connect (changed => sub {
  if ($compression[$combob->get_active][0] eq 'jpg') {
   $hboxq -> show_all;
  }
  else {
   $hboxq -> hide_all;
  }
 });
 combobox_set_active($combob, $SETTING{'pdf compression'}, @compression);
 $hbox -> pack_end ($combob, FALSE, FALSE, 0);

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


# Display page selector and on save a fileselector.

sub save_dialog {

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

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

 ($windowi, my $vbox) = create_window($window, $d->get('Save'), 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 = (
  [ 'pdf', $d->get('PDF'), $d->get('Portable Document Format') ],
  [ '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') ],
  [ 'txt', $d->get('Text'), $d->get('Plain text') ],
  [ 'session', $d->get('Session'), $d->get('gscan2pdf session file') ],
 );
 push @type, [ 'djvu', $d->get('DjVu'), $d->get('Deja Vu') ]
  if $dependencies{djvu};

 my @tiff_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 rather than jpg needed here because tiffcp uses -c jpeg
  [ '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, FALSE, FALSE, 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 ($hboxtq, $spinbuttontq) = add_quality_spinbutton($vbox);
$label = Gtk2::Label -> new ($d->get('tiff'));
$hboxtq -> pack_start ($label, FALSE, FALSE, 0);

# Fill compression ComboBox
 my $combobtc = combobox_from_array(@tiff_compression);
 $combobtc -> signal_connect (changed => sub {
  if ($tiff_compression[$combobtc->get_active][0] eq 'jpeg') {
   $hboxtq -> show_all;
  }
  else {
   $hboxtq -> hide_all;
   $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
  }
 });
 combobox_set_active($combobtc, $SETTING{'tiff compression'}, @tiff_compression);
 $hboxc -> pack_end ($combobtc, FALSE, FALSE, 0);

# PDF options
 my $vboxp = Gtk2::VBox -> new;
 $vbox -> pack_start($vboxp, FALSE, FALSE, 0);
 my ($entrya, $entryt, $entrys, $entryk) = add_PDF_options ($vboxp);

# Compression options
 my ($buttond, $spinbuttond, $combob, $hboxpq, $spinbuttonpq, @pdf_compression) = add_pdf_compression($vboxp);

# Fill image type ComboBox
 my $combobi = combobox_from_array(@type);
 $combobi -> signal_connect (changed => sub {
  if ($type[$combobi->get_active][0] eq 'pdf') {
   $vboxp -> show_all;
   if ($pdf_compression[$combob->get_active][0] eq 'jpg') {
    $hboxpq -> show_all;
   }
   else {
    $hboxpq -> hide_all;
    $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
   }
  }
  else {
   $vboxp -> hide_all;
   $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
  }
  if ($type[$combobi->get_active][0] eq 'tif') {
   $hboxc -> show_all;
  }
  else {
   $hboxc -> hide_all;
   $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
  }
  if ($type[$combobi->get_active][0] eq 'jpg'
      or ($type[$combobi->get_active][0] eq 'tif'
          and $tiff_compression[$combobtc->get_active][0] eq 'jpeg')) {
   $hboxtq -> show_all;
  }
  else {
   $hboxtq -> hide_all;
   $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
  }
 });
 combobox_set_active($combobi, $SETTING{'image type'}, @type);
 $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];

  if ($SETTING{'image type'} eq 'pdf') {

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

   update_PDF_settings($entrya, $entryt, $entrys, $entryk);
   save_PDF();
  }
  elsif ($SETTING{'image type'} eq 'djvu') {
   save_djvu();
  }
  elsif ($SETTING{'image type'} eq 'tif') {
   $SETTING{'tiff compression'} = $tiff_compression[$combobtc->get_active][0];
   $SETTING{'quality'} = $spinbuttontq->get_value;

# 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');
   $file_chooser -> set_do_overwrite_confirmation (TRUE);
   $file_chooser -> signal_connect (response => sub {
    my ($dialog, $response) = @_;
    if ($response eq 'ok') {
     my $filename = $file_chooser -> get_filename;
     if ($filename !~ /\.tif$/i) {
      $filename = $filename.'.tif';
      if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
       $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
       Glib::Idle->add (sub { $file_chooser->response('ok'); });
       return;
      }
     }

# Check that the file can be written
     if (! -w dirname($filename)) {
      show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
      return;
     }

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

     save_TIFF($filename);
    }
    $file_chooser -> destroy;
   });
   $file_chooser -> show;

# cd back to tempdir
   chdir $SETTING{session};
  }
  elsif ($SETTING{'image type'} eq 'txt') {

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

# Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog -> new($d->get('Text filename'),
                                                     $windowi, 'save',
                                                     'gtk-cancel' => 'cancel',
                                                     'gtk-save' => 'ok');
   $file_chooser -> set_default_response('ok');
   $file_chooser -> set_do_overwrite_confirmation (TRUE);
   $file_chooser -> signal_connect (response => sub {
    my ($dialog, $response) = @_;
    if ($response eq 'ok') {
     my $filename = $file_chooser -> get_filename;
     if ($filename !~ /\.txt$/i) {
      $filename = $filename.'.txt';
      if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
       $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
       Glib::Idle->add (sub { $file_chooser->response('ok'); });
       return;
      }
     }

# Check that the file can be written
     if (! -w dirname($filename)) {
      show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
      return;
     }

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

     open my $fh, ">:utf8", $filename
                      or die sprintf($d->get("Can't open file: %s"), $filename);
     my @pagelist = get_page_index();
     for (my $i = 0; $i < @pagelist; $i++) {
      print $fh $slist -> {data}[$pagelist[$i]][3];
     }
     close $fh;
    }
    $file_chooser -> destroy;
   });
   $file_chooser -> show;

# cd back to tempdir
   chdir $SETTING{session};
  }
  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'),
                                                      $windowi, 'save',
                                                      'gtk-cancel' => 'cancel',
                                                      'gtk-save' => 'ok');
   $file_chooser -> set_default_response('ok');
   $file_chooser -> set_do_overwrite_confirmation (TRUE);
   $file_chooser -> signal_connect (response => sub {
    my ($dialog, $response) = @_;
    if ($response eq 'ok') {
     my $filename = $file_chooser -> get_filename;
     if ($filename !~ /\.ps$/i) {
      $filename = $filename.'.ps';
      if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
       $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
       Glib::Idle->add (sub { $file_chooser->response('ok'); });
       return;
      }
     }

# Check that the file can be written
     if (! -w dirname($filename)) {
      show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
      return;
     }

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

# Create the PS
     my (undef, $tif) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
     save_TIFF($tif, $filename);
     unlink $tif;
    }
    $file_chooser -> destroy;
   });
   $file_chooser -> show;

# cd back to tempdir
   chdir $SETTING{session};
  }
  elsif ($SETTING{'image type'} eq 'session') {

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

# Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog -> new($d->get('gscan2pdf session filename'),
                                                      $windowi, 'save',
                                                      'gtk-cancel' => 'cancel',
                                                      'gtk-save' => 'ok');
   $file_chooser -> set_default_response('ok');
   $file_chooser -> set_do_overwrite_confirmation (TRUE);
   $file_chooser -> signal_connect (response => sub {
    my ($dialog, $response) = @_;
    if ($response eq 'ok') {
     my $filename = $file_chooser -> get_filename;
     if ($filename !~ /\.gs2p$/i) {
      $filename = $filename.'.gs2p';
      if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
       $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
       Glib::Idle->add (sub { $file_chooser->response('ok'); });
       return;
      }
     }

# Check that the file can be written
     if (! -w dirname($filename)) {
      show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
      return;
     }

# Update cwd
     $SETTING{'cwd'} = dirname($filename);
     save_session($filename);
    }
    $file_chooser -> destroy;
   });
   $file_chooser -> show;

# cd back to tempdir
   chdir $SETTING{session};
  }
  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;
 $hboxpq -> hide_all if ($pdf_compression[$combob->get_active][0] ne 'jpg');
 $hboxtq -> hide_all if ($type[$combobi->get_active][0] ne 'jpg'
                        or ($type[$combobi->get_active][0] eq 'tif'
                     and $tiff_compression[$combobtc->get_active][0] ne 'jpg'));
 $hboxc -> hide_all if ($type[$combobi->get_active][0] ne 'tif');
 $vboxp -> hide_all if ($type[$combobi->get_active][0] ne 'pdf');
 $windowi -> resize(100, 100); # Doesn't matter that 200x200 is too small
 return;
}


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 $SETTING{session};

# 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)) {
    my $cmd = "convert $filelist[0] -density $slist->{data}[$pagelist[0]][4] '$filename'";
    print "$cmd\n" if $debug;
    if (! system ($cmd)) {
     $windowi -> hide if defined $windowi;
     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'}")) {
     my $cmd = sprintf "convert %s -density %d \"%s\"",
      $_, $slist->{data}[$pagelist[$i-1]][4], "$filename.$i.$SETTING{'image type'}";
     if (system ($cmd)) {
      show_message_dialog($window, 'error', 'close', $d->get('Error saving image'));
     }
     else {
      $windowi -> hide if defined $windowi;
      mark_pages(@pagelist);
     }
    }
    $i++;
   }
  }
 }

 $file_chooser -> destroy;
 return;
}


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

 my $dialog = Gtk2::Dialog -> new ($d->get('Saving TIFF')."...", $window,
                                   'modal',
                                   '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 ($child, $parent) = open_socketpair();
 my $pid = start_process(sub {

# fill $pagelist with filenames depending on which radiobutton is active
  my @pagelist = get_page_index();
  my $pagelist = '';
  my $page = 0;
  for (my $i = 0; $i < @pagelist; $i++) {
   my $j = $pagelist[$i];

   ++$page;
   send($parent, ($page-1)/($#pagelist+2)
    .sprintf($d->get("Converting image %i of %i to TIFF"), $page, $#pagelist+1), 0);
   my $filename = $slist -> {data}[$j][2];
   if ($filename !~ /\.tif/ or $SETTING{'tiff compression'} eq 'jpeg') {
    my (undef, $tif) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
    my $resolution = $slist -> {data}[$j][4];

# Convert to tiff
    my $depth = '';
    $depth = '-depth 8' if ($SETTING{'tiff compression'} eq 'jpeg');
    system ("convert -units PixelsPerInch -density $resolution $depth $filename $tif") == 0 or
     send($parent, "2".$d->get('Error writing TIFF'), 0);

    $filename = $tif;
   }
   $pagelist .= " $filename";
  }

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

# Create the tiff
  send($parent, '1'.$d->get('Concatenating TIFFs'), 0);
  my $rows = '';
  $rows = '-r 16' if ($SETTING{'tiff compression'} eq 'jpeg');
  my $cmd = "tiffcp $rows -c $compression $pagelist \"$filename\"";
  print "$cmd\n" if $debug;
  my $status = system("$cmd 2>$SETTING{session}/tiffcp.stdout");
  my $output = '';
  if ($status != 0) {
   $output = slurp("$SETTING{session}/tiffcp.stdout");
   print $output if $debug;
   send($parent, '-1'.sprintf($d->get("Error compressing image: %s"), $output), 0);
  }
  if (defined $ps) {
   send($parent, '1'.$d->get('Converting to PS'), 0);
   my $cmd = "tiff2ps $filename > $ps";
   print "$cmd\n" if $debug;
   $output = `$cmd`;
  }
  send($parent, "2$output", 0);
 });

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($child, $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
    }
    if ($fraction > -1) {
     $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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   update_uimanager();
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 return;
}


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'),
                                                      $windowi, 'save',
                                                      'gtk-cancel' => 'cancel',
                                                      'gtk-save' => 'ok');
 $file_chooser -> set_default_response('ok');
 $file_chooser -> set_do_overwrite_confirmation (TRUE);
 $file_chooser -> signal_connect (response => sub {
  my ($dialog, $response) = @_;
  if ($response eq 'ok') {
   my $filename = $file_chooser -> get_filename;
   if ($filename !~ /\.djvu$/i) {
    $filename = $filename.'.djvu';
    if (-f $filename) {
# File exists; get the file chooser to ask the user to confirm.
     $file_chooser -> set_filename($filename);
# Give the name change time to take effect.
     Glib::Idle->add (sub { $file_chooser->response('ok'); });
     return;
    }
   }

# Check that the file can be written
   if (! -w dirname($filename)) {
    show_message_dialog($file_chooser, 'error', 'close',
                           sprintf($d->get("File %s is read-only"), $filename));
    return;
   }

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

# cd back to tempdir
   chdir $SETTING{session};

   my $dialog = Gtk2::Dialog -> new ($d->get('Saving DjVu')."...", $windowi,
                                     'modal',
                                     '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 ($child, $parent) = open_socketpair();
   my $pid = start_process(sub {

    my @filelist;
   
    for (my $i = 0; $i < @pagelist; $i++) {
     my $j = $pagelist[$i];
     send($parent, $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 => $SETTING{session}, 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 $class = $image->Get('class');
     my $compression;

# c44 can only use pnm and jpg
     my $format;
     $format = $1 if ($filename =~ /\.(\w*)$/);
     if ($depth > 1) {
      $compression = 'c44';
      if ($format !~ /(pnm|jpg)/) {
       my (undef, $pnm) = tempfile(DIR => $SETTING{session}, 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)/ or ($format eq 'pnm' and $class ne 'PseudoClass')) {
       my (undef, $pbm) = tempfile(DIR => $SETTING{session}, SUFFIX => '.pbm');
       $x = $image->Write(filename => $pbm);
       warn "$x" if "$x";
       $filename = $pbm;
      }
     }

# 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($parent, "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 my $fh, ">:utf8", $djvusedtxtfile
       or die sprintf($d->get("Can't open file: %s"), $djvusedtxtfile);
      print $fh "(page 0 0 $w $h\n";
      print $fh "(line 0 0 $w $h \"";
      print $fh $txt, "\"))";
      close $fh;

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

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

    my $line;
    if ($condition & 'in') { # bit field operation. >= would also work
     recv($child, $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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
     $dialog -> destroy;
     update_uimanager();
     return FALSE;  # uninstall
    }
    return TRUE;  # continue without uninstalling
   });
  }
  $file_chooser -> destroy;

 });
 $file_chooser -> show;

# cd back to tempdir
 chdir $SETTING{session};
 return;
}


# 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 => $SETTING{session}, SUFFIX => '.pdf');

# Create the PDF
  create_PDF($pdf);

# Check for thunderbird
  my ($client, $status);
  if (defined($ENV{KDE_FULL_SESSION}) and $ENV{KDE_FULL_SESSION} eq 'true') {
   $client=`kreadconfig --file emaildefaults --group PROFILE_Default --key EmailClient| cut -d ' ' -f 1`;
  }
  elsif (defined($ENV{GNOME_DESKTOP_SESSION_ID})
   and $ENV{GNOME_DESKTOP_SESSION_ID} ne '') {
   $client=`gconftool --get /desktop/gnome/url-handlers/mailto/command | cut -d ' ' -f 1`;
  }
  if ($client =~ /thunderbird/) {
   $status = system("thunderbird -compose attachment=file://$pdf");
  }
  else {
   $status = system("xdg-email --attach $pdf 'x\@y'");
  }
  show_message_dialog($window, 'error', 'close', $d->get('Error creating email'))
   if ($status);

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


# 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;
 $notebook->set_scrollable (TRUE);
 $vbox->pack_start($notebook, TRUE, TRUE, 0);

# Notebook page 1
 my $vbox1 = Gtk2::VBox->new;
 $notebook->append_page($vbox1, $d->get('Page 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);
 $bscanall -> signal_connect (clicked => sub {
  $batch_scan -> set_active(0) if (defined $batch_scan);
 });

# 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, 999, 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 ($SETTING{'pages to scan'} eq 'all') {
  $bscanall -> set_active(TRUE);
 }
 else {
  $bscannum -> set_active(TRUE);
  $spin_buttonn -> set_value($SETTING{'pages to scan'});
 }

# 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
  my $n = $spin_buttonn -> get_value;
  $batch_scan -> set_active(0) if (defined($batch_scan) and $n > 1);

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

# Frame for standard mode
 $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);

# Rotate
 my $hboxr = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxr, FALSE, FALSE, 0);
 my $rbutton = Gtk2::CheckButton -> new($d->get('Rotate'));
 $tooltips -> set_tip ($rbutton, $d->get('Rotate image after scanning'));
 $hboxr -> pack_start($rbutton, TRUE, TRUE, 0);
 @side = (
  [ 'both', $d->get('Both sides'), $d->get('Both sides.') ],
  [ 'facing', $d->get('Facing side'), $d->get('Facing side.') ],
  [ 'reverse', $d->get('Reverse side'), $d->get('Reverse side.') ],
 );
 my $comboboxs = combobox_from_array(@side);
 $tooltips -> set_tip ($comboboxs, $d->get('Select side to rotate'));
 $hboxr -> pack_start($comboboxs, TRUE, TRUE, 0);
 my @rotate = (
  [ 90, $d->get('90'), $d->get('Rotate image 90 degrees clockwise.') ],
  [ 180, $d->get('180'), $d->get('Rotate image 180 degrees clockwise.') ],
  [ 270, $d->get('270'), $d->get('Rotate image 90 degrees anticlockwise.') ],
 );
 my $comboboxr = combobox_from_array(@rotate);
 $tooltips -> set_tip ($comboboxr, $d->get('Select direction of rotation'));
 $hboxr -> pack_end($comboboxr, TRUE, TRUE, 0);

 $hboxr = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxr, FALSE, FALSE, 0);
 my $r2button = Gtk2::CheckButton -> new($d->get('Rotate'));
 $tooltips -> set_tip ($r2button, $d->get('Rotate image after scanning'));
 $hboxr -> pack_start($r2button, TRUE, TRUE, 0);
 my @side2;
 my $comboboxs2 = Gtk2::ComboBox->new_text;
 $tooltips -> set_tip ($comboboxs2, $d->get('Select side to rotate'));
 $hboxr -> pack_start($comboboxs2, TRUE, TRUE, 0);
 my $comboboxr2 = combobox_from_array(@rotate);
 $tooltips -> set_tip ($comboboxr2, $d->get('Select direction of rotation'));
 $hboxr -> pack_end($comboboxr2, TRUE, TRUE, 0);

 $rbutton -> signal_connect (toggled => sub {
  if ($rbutton -> get_active) {
   $hboxr->set_sensitive(TRUE)
    if ($side[$comboboxs -> get_active]->[0] ne 'both');
  }
  else {
   $hboxr->set_sensitive(FALSE);
  }
 });
 $comboboxs -> signal_connect (changed => sub {
  if ($side[$comboboxs -> get_active]->[0] eq 'both') {
   $hboxr->set_sensitive(FALSE);
   $r2button->set_active(FALSE);
  }
  else {
   $hboxr->set_sensitive(TRUE) if ($rbutton -> get_active);

# Empty combobox
   while ($comboboxs2->get_active > -1) {
    $comboboxs2->remove_text(0);
    $comboboxs2->set_active(0);
   }
   @side2 = ();
   foreach (@side) {
    push @side2, $_
     unless ($_->[0] eq 'both'
                            or $_->[0] eq $side[$comboboxs -> get_active]->[0]);
   }
   $comboboxs2->append_text ($side2[0]->[1]);
   $comboboxs2->set_active(0);
  }
 });

# In case it isn't set elsewhere
 combobox_set_active($comboboxr2, 90, @rotate);

 if ($SETTING{'rotate facing'} or $SETTING{'rotate reverse'}) {
  $rbutton->set_active(TRUE);
 }
 if ($SETTING{'rotate facing'} == $SETTING{'rotate reverse'}) {
  combobox_set_active($comboboxs, 'both', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate facing'}, @rotate);
 }
 elsif ($SETTING{'rotate facing'}) {
  combobox_set_active($comboboxs, 'facing', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate facing'}, @rotate);
  if ($SETTING{'rotate reverse'}) {
   $r2button->set_active(TRUE);
   combobox_set_active($comboboxs2, 'reverse', @side2);
   combobox_set_active($comboboxr2, $SETTING{'rotate reverse'}, @rotate);
  }
 }
 else {
  combobox_set_active($comboboxs, 'reverse', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate reverse'}, @rotate);
 }

# CheckButton for unpaper
 my $hboxu = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxu, FALSE, FALSE, 0);
 my $ubutton = Gtk2::CheckButton -> new($d->get('Clean up images'));
 $tooltips -> set_tip ($ubutton, $d->get('Clean up scanned images with unpaper'));
 $hboxu -> pack_start($ubutton, TRUE, TRUE, 0);
 if (! $dependencies{unpaper}) {
  $ubutton -> set_sensitive(FALSE);
  $ubutton -> set_active(FALSE);
 }
 elsif ($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 (not $dependencies{gocr} and not $dependencies{tesseract}) {
  $hboxo -> set_sensitive(FALSE);
  $obutton -> set_active(FALSE);
 }
 elsif ($SETTING{'OCR on scan'}) {
  $obutton -> set_active(TRUE);
 }
 $hboxo -> pack_start($obutton, TRUE, TRUE, 0);
 my $comboboxe = combobox_from_array(@ocr_engine);
 $tooltips -> set_tip ($comboboxe, $d->get('Select OCR engine'));
 $hboxo -> pack_end($comboboxe, TRUE, TRUE, 0);
 my ($comboboxl, @tesslang, $hboxl);
 if ($dependencies{tesseract}) {
  ($hboxl, $comboboxl, @tesslang) = add_ocr_languages($vboxp);
  $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);
   }
  });
 }
 combobox_set_active($comboboxe, $SETTING{'ocr engine'}, @ocr_engine);

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

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

# Scan profiles
 my $framesp = Gtk2::Frame -> new($d->get('Scan profiles'));
 $vbox2 -> pack_start ($framesp, FALSE, FALSE, 0);
 my $vboxsp = Gtk2::VBox -> new;
 $vboxsp -> set_border_width($border_width);
 $framesp -> add ($vboxsp);
 my $combobsp = Gtk2::ComboBox -> new_text;
 foreach my $profile (keys %{$SETTING{profile}}) {
  $combobsp->append_text($profile);
 }
 $combobsp -> signal_connect (changed => sub {
  my $profile = $combobsp->get_active_text;
  if (defined $profile) {
   foreach my $key (keys %{$SETTING{profile}{$profile}}) {
    $SETTING{$key} = $SETTING{profile}{$profile}{$key};
   }
   rescan_options($vboxd, $device[$combobd -> get_active]) if (defined $combobd);
  }
 });
 $combobsp->set_active(num_rows_combobox($combobsp));
 $vboxsp -> pack_start ($combobsp, FALSE, FALSE, 0);
 my $hboxsp = Gtk2::HBox -> new;
 $vboxsp -> pack_end ($hboxsp, FALSE, FALSE, 0);

# Save button
 my $vbutton = Gtk2::Button -> new_from_stock('gtk-save');
 $vbutton -> signal_connect (clicked => sub {
  my $dialog = Gtk2::Dialog->new ($d->get('Name of scan profile'), $windows,
                                          'destroy-with-parent',
                                          'gtk-save' => 'ok',
                                          'gtk-cancel' => 'cancel');
  my $hbox = Gtk2::HBox -> new;
  my $label = Gtk2::Label->new ($d->get('Name of scan profile'));
  $hbox -> pack_start( $label, FALSE, FALSE, 0 );
  my $entry = Gtk2::Entry -> new;
  $entry->set_activates_default(TRUE);
  $hbox -> pack_end( $entry, TRUE, TRUE, 0 );
  $dialog->vbox->add($hbox);
  $dialog->set_default_response ('ok');
  $dialog->show_all;
  if ($dialog->run eq 'ok' and $entry->get_text !~ /^\s*$/) {
   my $profile = $entry->get_text;
   my %options = walk_options_tree();
   $combobsp->append_text($profile);
   $SETTING{profile}{$profile} = \%options;
   $combobsp->set_active(num_rows_combobox($combobsp));
  }
  $dialog->destroy;
 });
 $hboxsp -> pack_start( $vbutton, TRUE, TRUE, 0 );

# Delete button
 my $dbutton = Gtk2::Button -> new_from_stock('gtk-delete');
 $dbutton -> signal_connect (clicked => sub {
  my $i = $combobsp->get_active;
  if ($i > -1) {
   delete $SETTING{profile}{$combobsp->get_active_text};
   $combobsp->remove_text($i);
   $combobsp->set_active($i) if (num_rows_combobox($combobsp) > -1);
  }
 });
 $hboxsp -> pack_start( $dbutton, FALSE, FALSE, 0 );

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

# ignore artificial paper size option
  my %options = walk_options_tree();
  delete $options{'Paper size'} if (defined $options{'Paper size'});

# 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 (not $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{'rotate facing'} = 0;
  $SETTING{'rotate reverse'} = 0;
  if ($rbutton->get_active) {
   if ($side[$comboboxs -> get_active]->[0] eq 'both') {
    $SETTING{'rotate facing'} = $rotate[$comboboxr -> get_active]->[0];
    $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
   }
   elsif ($side[$comboboxs -> get_active]->[0] eq 'facing') {
    $SETTING{'rotate facing'} = $rotate[$comboboxr -> get_active]->[0];
   }
   else {
    $SETTING{'rotate reverse'} = $rotate[$comboboxr -> get_active]->[0];
   }
   if ($r2button->get_active) {
    if ($side2[$comboboxs2 -> get_active]->[0] eq 'facing') {
     $SETTING{'rotate facing'} = $rotate[$comboboxr2 -> get_active]->[0];
    }
    else {
     $SETTING{'rotate reverse'} = $rotate[$comboboxr2 -> get_active]->[0];
    }
   }
  }
  print "rotate facing $SETTING{'rotate facing'}\n" if $debug;
  print "rotate reverse $SETTING{'rotate reverse'}\n" if $debug;
  my $rotate_facing = $SETTING{'rotate facing'};
  my $rotate_reverse = $SETTING{'rotate reverse'};
  print $duplex ? "" : "non-", "duplex mode\n" if $debug;
  if (! $duplex) {
   if ($step > 0) {
    $rotate_reverse = $SETTING{'rotate facing'};
   }
   else {
    $rotate_facing = $SETTING{'rotate reverse'};
   }
  }
  print "rotate_facing $rotate_facing\n" if $debug;
  print "rotate_reverse $rotate_reverse\n" if $debug;

  $SETTING{'unpaper on scan'} = $ubutton->get_active;
  print "unpaper $SETTING{'unpaper on scan'}\n" if $debug;
  $SETTING{'OCR on scan'} = $obutton->get_active;
  print "OCR $SETTING{'OCR on scan'}\n" if $debug;
  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' or $SETTING{frontend} eq 'scanimage-perl') {
   scanimage($SETTING{device}, $npages, $start-$step, $step,
                $rotate_facing, $rotate_reverse,
                $SETTING{'unpaper on scan'}, $SETTING{'OCR on scan'}, %options);
  }
  else {
   scanadf($SETTING{device}, $npages, $start-$step, $step,
                $rotate_facing, $rotate_reverse,
                $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; } );

 if ($test) {
  parse_device_list($test->{output});
  populate_device_list($hboxd);
 }
 elsif (! @device) {
  get_devices($hboxd);
 }
 else {
  populate_device_list($hboxd);
 }

# Show window
 $windows -> show_all;
 $framex->hide_all;
 $hboxl->hide_all
  if ($dependencies{tesseract}
                   and $ocr_engine[$comboboxe -> get_active]->[0] ne 'tesseract');

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


# Run scanimage --formatted-device-list

sub get_devices {
 my $hboxd = shift;

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

 my $cmd = "$SETTING{'scan prefix'} 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
 my $output = '';
 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 (not 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 ($output eq '') {
    $windows->destroy;
    undef $windows;
    show_message_dialog($window, 'error', 'close', $d->get('No devices found'));
    return FALSE;
   }

   my @oldmodel = @model;
   parse_device_list($output);
   if (@oldmodel) { # Update combobox
    my $i = 0;
    while ($i < @oldmodel) {
     if (@model and $i < @model and $oldmodel[$i] ne $model[$i]) {
      $combobd->insert_text($i, $model[$i]);
      $combobd->remove_text($i);
     }
     $i++
    }
    while ($i < @model) {
     $combobd->append_text($model[$i++]);
    }
    set_device();
    $combobd->show;
    $labeld->show;
   }
   else { # New combobox
    populate_device_list($hboxd);
   }
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 return;
}


# Scan

sub scan_dialog2 {

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

# scan pop-up window
 ($windows2, 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
 $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'));

# 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, 999, 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 ($SETTING{'pages to scan'} eq 'all') {
  $bscanall -> set_active(TRUE);
 }
 else {
  $bscannum -> set_active(TRUE);
  $spin_buttonn -> set_value($SETTING{'pages to scan'});
 }

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

# Rotate
 my $hboxr = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxr, FALSE, FALSE, 0);
 my $rbutton = Gtk2::CheckButton -> new($d->get('Rotate'));
 $tooltips -> set_tip ($rbutton, $d->get('Rotate image after scanning'));
 $hboxr -> pack_start($rbutton, TRUE, TRUE, 0);
 @side = (
  [ 'both', $d->get('Both sides'), $d->get('Both sides.') ],
  [ 'facing', $d->get('Facing side'), $d->get('Facing side.') ],
  [ 'reverse', $d->get('Reverse side'), $d->get('Reverse side.') ],
 );
 my $comboboxs = combobox_from_array(@side);
 $tooltips -> set_tip ($comboboxs, $d->get('Select side to rotate'));
 $hboxr -> pack_start($comboboxs, TRUE, TRUE, 0);
 my @rotate = (
  [ 90, $d->get('90'), $d->get('Rotate image 90 degrees clockwise.') ],
  [ 180, $d->get('180'), $d->get('Rotate image 180 degrees clockwise.') ],
  [ 270, $d->get('270'), $d->get('Rotate image 90 degrees anticlockwise.') ],
 );
 my $comboboxr = combobox_from_array(@rotate);
 $tooltips -> set_tip ($comboboxr, $d->get('Select direction of rotation'));
 $hboxr -> pack_end($comboboxr, TRUE, TRUE, 0);

 $hboxr = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxr, FALSE, FALSE, 0);
 my $r2button = Gtk2::CheckButton -> new($d->get('Rotate'));
 $tooltips -> set_tip ($r2button, $d->get('Rotate image after scanning'));
 $hboxr -> pack_start($r2button, TRUE, TRUE, 0);
 my @side2;
 my $comboboxs2 = Gtk2::ComboBox->new_text;
 $tooltips -> set_tip ($comboboxs2, $d->get('Select side to rotate'));
 $hboxr -> pack_start($comboboxs2, TRUE, TRUE, 0);
 my $comboboxr2 = combobox_from_array(@rotate);
 $tooltips -> set_tip ($comboboxr2, $d->get('Select direction of rotation'));
 $hboxr -> pack_end($comboboxr2, TRUE, TRUE, 0);

 $rbutton -> signal_connect (toggled => sub {
  if ($rbutton -> get_active) {
   $hboxr->set_sensitive(TRUE)
    if ($side[$comboboxs -> get_active]->[0] ne 'both');
  }
  else {
   $hboxr->set_sensitive(FALSE);
  }
 });
 $comboboxs -> signal_connect (changed => sub {
  if ($side[$comboboxs -> get_active]->[0] eq 'both') {
   $hboxr->set_sensitive(FALSE);
   $r2button->set_active(FALSE);
  }
  else {
   $hboxr->set_sensitive(TRUE) if ($rbutton -> get_active);

# Empty combobox
   while ($comboboxs2->get_active > -1) {
    $comboboxs2->remove_text(0);
    $comboboxs2->set_active(0);
   }
   @side2 = ();
   foreach (@side) {
    push @side2, $_
     unless ($_->[0] eq 'both'
                            or $_->[0] eq $side[$comboboxs -> get_active]->[0]);
   }
   $comboboxs2->append_text ($side2[0]->[1]);
   $comboboxs2->set_active(0);
  }
 });

# In case it isn't set elsewhere
 combobox_set_active($comboboxr2, 90, @rotate);

 if ($SETTING{'rotate facing'} or $SETTING{'rotate reverse'}) {
  $rbutton->set_active(TRUE);
 }
 if ($SETTING{'rotate facing'} == $SETTING{'rotate reverse'}) {
  combobox_set_active($comboboxs, 'both', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate facing'}, @rotate);
 }
 elsif ($SETTING{'rotate facing'}) {
  combobox_set_active($comboboxs, 'facing', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate facing'}, @rotate);
  if ($SETTING{'rotate reverse'}) {
   $r2button->set_active(TRUE);
   combobox_set_active($comboboxs2, 'reverse', @side2);
   combobox_set_active($comboboxr2, $SETTING{'rotate reverse'}, @rotate);
  }
 }
 else {
  combobox_set_active($comboboxs, 'reverse', @side);
  combobox_set_active($comboboxr, $SETTING{'rotate reverse'}, @rotate);
 }

# CheckButton for unpaper
 my $hboxu = Gtk2::HBox -> new;
 $vboxp -> pack_start ($hboxu, FALSE, FALSE, 0);
 my $ubutton = Gtk2::CheckButton -> new($d->get('Clean up images'));
 $tooltips -> set_tip ($ubutton, $d->get('Clean up scanned images with unpaper'));
 $hboxu -> pack_start($ubutton, TRUE, TRUE, 0);
 if (! $dependencies{unpaper}) {
  $ubutton -> set_sensitive(FALSE);
  $ubutton -> set_active(FALSE);
 }
 elsif ($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 (not $dependencies{gocr} and not $dependencies{tesseract}) {
  $hboxo -> set_sensitive(FALSE);
  $obutton -> set_active(FALSE);
 }
 elsif ($SETTING{'OCR on scan'}) {
  $obutton -> set_active(TRUE);
 }
 $hboxo -> pack_start($obutton, TRUE, TRUE, 0);
 my $comboboxe = combobox_from_array(@ocr_engine);
 $tooltips -> set_tip ($comboboxe, $d->get('Select OCR engine'));
 $hboxo -> pack_end($comboboxe, TRUE, TRUE, 0);
 my ($comboboxl, @tesslang, $hboxl);
 if ($dependencies{tesseract}) {
  ($hboxl, $comboboxl, @tesslang) = add_ocr_languages($vboxp);
  $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);
   }
  });
 }
 combobox_set_active($comboboxe, $SETTING{'ocr engine'}, @ocr_engine);

# 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 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 (not $checkx -> get_active and $start == 1 and $step < 0) {
   show_message_dialog($windows2, 'error', 'cancel', $d->get('Must scan facing pages first'));
   return TRUE;
  }

  $SETTING{'rotate facing'} = 0;
  $SETTING{'rotate reverse'} = 0;
  if ($rbutton->get_active) {
   if ($side[$comboboxs -> get_active]->[0] eq 'both') {
    $SETTING{'rotate facing'} = $rotate[$comboboxr -> get_active]->[0];
    $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
   }
   elsif ($side[$comboboxs -> get_active]->[0] eq 'facing') {
    $SETTING{'rotate facing'} = $rotate[$comboboxr -> get_active]->[0];
   }
   else {
    $SETTING{'rotate reverse'} = $rotate[$comboboxr -> get_active]->[0];
   }
   if ($r2button->get_active) {
    if ($side2[$comboboxs2 -> get_active]->[0] eq 'facing') {
     $SETTING{'rotate facing'} = $rotate[$comboboxr2 -> get_active]->[0];
    }
    else {
     $SETTING{'rotate reverse'} = $rotate[$comboboxr2 -> get_active]->[0];
    }
   }
  }
  print "rotate facing $SETTING{'rotate facing'}\n" if $debug;
  print "rotate reverse $SETTING{'rotate reverse'}\n" if $debug;
  my $rotate_facing = $SETTING{'rotate facing'};
  my $rotate_reverse = $SETTING{'rotate reverse'};
  print $duplex ? "" : "non-", "duplex mode\n" if $debug;
  if (! $duplex) {
   if ($step > 0) {
    $rotate_reverse = $SETTING{'rotate facing'};
   }
   else {
    $rotate_facing = $SETTING{'rotate reverse'};
   }
  }
  print "rotate_facing $rotate_facing\n" if $debug;
  print "rotate_reverse $rotate_reverse\n" if $debug;

  $SETTING{'unpaper on scan'} = $ubutton->get_active;
  print "unpaper $SETTING{'unpaper on scan'}\n" if $debug;
  $SETTING{'OCR on scan'} = $obutton->get_active;
  print "OCR $SETTING{'OCR on scan'}\n" if $debug;
  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');
  }
  scan_pages($npages, $start, $step, $rotate_facing, $rotate_reverse,
                $SETTING{'unpaper on scan'}, $SETTING{'OCR on scan'});
 } );

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

 if (@device_list) {
  populate_device_list2($hboxd);
 }
 else {
  get_devices2($hboxd);
 }

# Show window
 $windows2 -> show_all;
 $framex->hide_all;
 $hboxl->hide_all
  if ($dependencies{tesseract}
                   and $ocr_engine[$comboboxe -> get_active]->[0] ne 'tesseract');
 return;
}


# Run Sane->get_devices

sub get_devices2 {
 my $hboxd = shift;

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

 {
  lock($shash{$sane_thread}{process});
  $shash{$sane_thread}{process} = '
   my @devices = Sane->get_devices;
   $shash{$sane_thread}{data} = freeze(\@devices);
   $shash{$sane_thread}{go} = 0;
  ';
  $shash{$sane_thread}{go} = 1;
  $shash{$sane_thread}{semaphore}->up;
 }

# Timer will run until callback returns false 
 my $timer = Glib::Timeout->add (100, sub {
  if ($shash{$sane_thread}{go}) {
   $pbar->pulse;
   return TRUE;
  }
  else {
   my @old_device_list = @device_list;
   @device_list = @{thaw($shash{$sane_thread}{data})};
   print "Sane->get_devices returned: ", Dumper(\@device_list) if $debug;
   if (@device_list == 0) {
    $windows2->destroy;
    undef $windows2;
    show_message_dialog($window, 'error', 'close', $d->get('No devices found'));
    return FALSE;
   }
   parse_device_list2();
   if (@old_device_list) { # Update combobox
    my $i = 0;
    while ($i < @old_device_list) {
     if (@device_list and $i < @device_list and $old_device_list[$i]->{label} ne $device_list[$i]->{label}) {
      $combobd->remove_text($i);
      $combobd->insert_text($i, $device_list[$i]->{label});
     }
     $i++
    }
    while ($i < @device_list) {
     $combobd->insert_text($i, $device_list[$i]->{label});
     $i++;
    }
    set_device2();
    $combobd->show;
    $labeld->show;
   }
   else { # New combobox
    populate_device_list2($hboxd);
   }
   $pbar -> destroy;
  }
 });
 return;
}


sub walk_options_tree {

# Get device-specific options
 my %options;
 if (defined $vboxm) {
  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')) {
      if ($key eq 'Paper size') {
       $SETTING{$key} = $widget -> get_active_text;
      }
      else {
       $SETTING{$key} = get_value(\%ddo, $key, $widget -> get_active_text);
      }
      $options{$key} = $SETTING{$key};
     }
     elsif ($widget -> isa('Gtk2::SpinButton')) {
      $options{$key} = $widget -> get_value;
      $SETTING{$key} = $options{$key};
     }
    }
   }
  }
 }
 warn Dumper(\%options) if ($debug);
 return %options;
}


# Get number of rows in combobox
sub num_rows_combobox {
 my ($combobox) = @_;
 my $i = -1;
 $combobox->get_model->foreach(sub {$i++;return FALSE});
 return $i;
}


sub parse_device_list {
 my ($output) = @_;

# clear lists
 @device = ();
 @model = ();

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

# parse out the device and model names
 my @words = &parse_line(',', 0, substr($output, 0, index($output, "'\n")+1));
 while (@words == 3) {
  $output = substr($output, index($output, "'\n")+2, length($output));
  $device[$words[0]] = $words[1];

# Convert all underscores to spaces
  ($model[$words[0]] = $words[2]) =~ s/_/ /g;
  @words = &parse_line(',', 0, substr($output, 0, index($output, "'\n")+1));
 }

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

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


sub parse_device_list2 {

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

# Note any duplicate model names and add the device if necessary
 undef %seen;
 for ( @device_list ) {
  $seen{ $_->{model} }++;
 }
 for ( @device_list ) {
  $_->{label} = "$_->{vendor} $_->{model}";
  $_->{label} .= " on $_->{name}" if ($seen{$_->{model}} > 1);
 }

# If device not set by config and there is a default device, then set it
 $SETTING{device} = $ENV{'SANE_DEFAULT_DEVICE'}
  if (not defined($SETTING{device}) and defined($ENV{'SANE_DEFAULT_DEVICE'}));
 return;
}


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

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

# read the model names into the combobox
 foreach (@model) {
  $combobd->append_text ($_);
 }
 $combobd->append_text ($d->get('Rescan for devices')) if (! $test);

# flags whether already run or not
 my $run = FALSE;
 $combobd -> signal_connect (changed => sub {
  my $index = $combobd -> get_active;
  if ($index > $#device) {
   $combobd->hide;
   $labeld->hide;
   get_devices($hboxd);
  }
  else {

# 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 in settings then set it
 set_device();
 $hboxd -> show_all;
 return;
}


sub set_device {
 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);
 return;
}


sub populate_device_list2 {
 my ($hboxd) = @_;

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

# read the model names into the combobox
 for (@device_list) {
  $combobd->append_text ($_->{label});
 }
 $combobd->append_text ($d->get('Rescan for devices')) if (! $test);

# flags whether already run or not
 my $run = FALSE;
 $combobd -> signal_connect (changed => sub {
  my $index = $combobd -> get_active;
  if ($index > $#device_list) {
   $combobd->hide;
   $labeld->hide;
   get_devices2($hboxd);
  }
  else {
   $SETTING{device} = $device_list[$combobd -> get_active]->{name};
   scan_options($notebook, $device_list[$combobd -> get_active]);
  }
 });
 $tooltips -> set_tip ($combobd, $d->get('Sets the device to be used for the scan'));
 $hboxd -> pack_end ($combobd, FALSE, FALSE, 0);

# If device in settings then set it
 set_device2();
 $hboxd -> show_all;
 return;
}


sub set_device2 {
 my $o;
 if (defined $SETTING{device}) {
  for (my $i = 0; $i < @device_list; $i++) {
   $o = $i if ($SETTING{device} eq $device_list[$i]->{name});
  }
 }
 $o = 0 unless (defined $o);

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


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


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


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

sub scanimage {
 my ($device, $npages, $offset, $step, $rfacing, $rreverse, $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 $SETTING{session};

# Create command
 my $cmd = "$SETTING{'scan prefix'} $SETTING{frontend} $device @options $npages";
 warn "$cmd\n" if $debug;

 if (! $test) {

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

# flag to ignore out of documents message if successfully scanned at least one page
  my $num_scans = 0;

# 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,
                                    'modal',
                                    '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;
  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
      my $index = import_scan ("out$1.pnm", $1*$step+$offset, 'Portable anymap',
                                                    $SETTING{resolution}, TRUE);
      if ($index == -1) {
       $dialog -> destroy;
       show_message_dialog(
        $windows, 'error', 'close', $d->get('Unable to load image')
       );
       return FALSE;
      }
      else {
       $num_scans++;
       my $rotate = $1 % 2 ? $rfacing : $rreverse;
       post_process_scan($index, $rotate, $unpaper, $ocr);
      }
     }
     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 =~ /^$SETTING{frontend}: sane_start: Document feeder out of documents/) {
      $dialog -> destroy;
      show_message_dialog($windows, 'info', 'close', $d->get('Document feeder out of documents'))
       if ($num_scans == 0);
     }
     elsif ($cancel
             and ($line =~ /^$SETTING{frontend}: sane_start: Error during device I\/O/
                  or $line =~ /^$SETTING{frontend}: received signal 2/
                  or $line =~ /^$SETTING{frontend}: trying to stop scanner/)) {
      ;
     }
     elsif ($line =~ /^$SETTING{frontend}: rounded/) {
      warn substr($line, 0, index($line, "\n")+1);
     }
     elsif ($line =~ /^$SETTING{frontend}: sane_start: Device busy/) {
      $dialog -> destroy;
      show_message_dialog($windows, 'error', 'close', $d->get('Device busy'));
     }
     elsif ($line =~ /^$SETTING{frontend}: sane_read: Operation was cancelled/) {
      $dialog -> destroy;
      show_message_dialog($windows, 'info', 'close', $d->get('Operation cancelled'));
     }
     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 (not 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, get on with post-processing
    $scanning = FALSE;
    post_process_scan();

    $dialog -> destroy;
    return FALSE;  # uninstall
   }
   return TRUE;  # continue without uninstalling
  });
  $scanning = TRUE;
 }
 else {
  my $index = import_scan ($test_image, 1*$step+$offset, 'Portable anymap',
                                                    $SETTING{resolution}, FALSE);
  if ($index == -1) {
   show_message_dialog(
    $windows, 'error', 'close', $d->get('Unable to load image')
   );
  }
  else {
   my $rotate = 1 % 2 ? $rfacing : $rreverse;
   post_process_scan($index, $rotate, $unpaper, $ocr);
  }
 }
 return;
}


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

sub scanadf {
 my ($device, $npages, $offset, $step, $rfacing, $rreverse, $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 $SETTING{session};

# Create command
 my $cmd = "$SETTING{'scan prefix'} $SETTING{frontend} $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,
                                    'modal',
                                    'gtk-cancel' => 'cancel');
  my $pbar = Gtk2::ProgressBar->new;
  $pbar -> set_pulse_step(.1);
  $dialog -> vbox -> add ($pbar);
  my $running = TRUE;

# Timer will run until callback returns false 
  my $size;
  my $id = 1;
  $pbar -> set_text(sprintf($d->get('Scanning page %i...'), $id*$step+$offset));
  my $timer = Glib::Timeout->add (100, sub { if ($running) {
                      if (defined $size) {
                       if ($size) {
                        $pbar->set_fraction((-s "out$id.pnm")/$size);
                       }
                       else {
                        $pbar->pulse;
                       }
                      }
                      elsif (-e "out$id.pnm" and (-s "out$id.pnm") > 50) {
                       $size = get_size_from_PNM("out$id.pnm")
                      }
                      else {
                       $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;
  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/) {
      $id = $1;

# Prevent the Glib::Timeout from checking the size of the file when it is
# about to be renamed
      undef $size;

# If the scan can't be loaded then blow the scanning dialog away and
# show an error
      my $index = import_scan ("out$id.pnm", $id*$step+$offset, 'Portable anymap',
                                                    $SETTING{resolution}, TRUE);
      if ($index == -1) {
       $dialog -> destroy;
       $running = FALSE;
       show_message_dialog(
        $windows, 'error', 'close', $d->get('Unable to load image')
       );
       return FALSE;
      }
      else {
       my $rotate = $id % 2 ? $rfacing : $rreverse;
       post_process_scan($index, $rotate, $unpaper, $ocr);
      }
      $id++;
      $pbar -> set_text(sprintf($d->get('Scanning page %i...'), $id*$step+$offset));
     }
     elsif ($line =~ /^Scanned \d* pages/) {
      ;
     }
     elsif ($line =~ /^$SETTING{frontend}: rounded/) {
      warn substr($line, 0, index($line, "\n")+1);
     }
     elsif ($line =~ /^$SETTING{frontend}: sane_start: Device busy/) {
      $dialog -> destroy;
      $running = FALSE;
      show_message_dialog($windows, 'error', 'close', $d->get('Device busy'));
     }
     elsif ($line =~ /^$SETTING{frontend}: sane_read: Operation was cancelled/) {
      $dialog -> destroy;
      $running = FALSE;
      show_message_dialog($windows, 'info', 'close', $d->get('Operation cancelled'));
     }
     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 (not 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
    $scanning = FALSE;
    post_process_scan();

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


sub scan_pages {
 my ($npages, $start, $step, $rfacing, $rreverse, $unpaper, $ocr) = @_;
 my $n = $start;
 my $format = "out%d.pnm";
 my $n2 = 1;
 my $npages2 = $npages;

# Set up ProgressBar
 my $dialog = Gtk2::Dialog -> new ($d->get('Scanning...'), $windows2,
                                           'modal',
                                           'gtk-cancel' => 'cancel');
 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;
  $rotating = FALSE;
  kill_subs();
 });
 $dialog -> show_all;
 if ($npages2 > 0) {
  $pbar->set_text (sprintf $d->get("Scanning page %d of %d"), $n2, $npages2);
 }
 else {
  $pbar->set_text (sprintf $d->get("Scanning page %d"), $n2);
 }

 new_page($format, $n);

# Timer will run until callback returns false 
 my $timer = Glib::Timeout->add (100, sub {
  if ($shash{$sane_thread}{go}) {
   $pbar->set_fraction ($shash{$sane_thread}{progress})
    if (defined $shash{$sane_thread}{progress});
   return TRUE;
  }
  else {
  
# Check status of scan
   if ($shash{$sane_thread}{status} == SANE_STATUS_GOOD or
       $shash{$sane_thread}{status} == SANE_STATUS_EOF) {
    $shash{$sane_thread}{status} = SANE_STATUS_GOOD;

# If the scan can't be loaded then blow the scanning dialog away and
# show an error
    my $index = import_scan (
     File::Spec->catdir($SETTING{session}, "out$n.pnm"), $n, 'Portable anymap',
                                                    $SETTING{resolution}, TRUE
    );
    if ($index == -1) {
     $dialog -> destroy;
     show_message_dialog(
      $windows, 'error', 'close', $d->get('Unable to load image')
     );
     return FALSE;
    }
    else {
     my $rotate = $n % 2 ? $rfacing : $rreverse;
     post_process_scan($index, $rotate, $unpaper, $ocr);
    }
   }
   else {
    $shash{$sane_thread}{data} = $path;
    $shash{$sane_thread}{process} = '
     my $path = $shash{$sane_thread}{data};
     close ($fh);
     unlink ($path);
     $shash{$sane_thread}{go} = 0;
    ';
    $shash{$sane_thread}{go} = 1;
    $shash{$sane_thread}{semaphore}->up;
   }

# Stop the process unless everything OK and more scans required
   unless (($npages == -1 or --$npages)
                        and $shash{$sane_thread}{status} == SANE_STATUS_GOOD) {
    $shash{$sane_thread}{process} = '
     $device->cancel;
     $shash{$sane_thread}{go} = 0;
    ';
    $shash{$sane_thread}{go} = 1;
    $shash{$sane_thread}{semaphore}->up;
    $dialog -> destroy;
    show_message_dialog($windows2, 'error', 'close',
                    $d_sane->get(Sane::strstatus($shash{$sane_thread}{status})))
     unless ($shash{$sane_thread}{status} == SANE_STATUS_GOOD
             or ($shash{$sane_thread}{status} == SANE_STATUS_NO_DOCS
                                                  and $npages < 1 and $n2 > 1));
    return FALSE;
   }

   $n += $step;
   $n2++;
   $pbar->set_fraction (0);
   if ($npages2 > 0) {
    $pbar->set_text (sprintf $d->get("Scanning page %d of %d"), $n2, $npages2);
   }
   else {
    $pbar->set_text (sprintf $d->get("Scanning page %d"), $n2);
   }
   new_page($format, $n);
   return TRUE;
  }
 });
 return;
}


sub new_page {
 my ($format, $n) = @_;
 my $path = sprintf $format, $n;
 $shash{$sane_thread}{data} = File::Spec->catdir($SETTING{session}, $path);
 $shash{$sane_thread}{process} = '
  $device->start;
  $shash{$sane_thread}{status} = $Sane::STATUS+0;
  if ($Sane::STATUS != SANE_STATUS_GOOD) {
   print "$prog_name: sane_start: $Sane::STATUS\n" if $debug;
   $shash{$sane_thread}{go} = 0;
   die; # quickest way out of the eval block
  }

  my $path = $shash{$sane_thread}{data};
  if (not open($fh, ">", $path)) {
   $device->cancel;
   $shash{$sane_thread}{status} = SANE_STATUS_ACCESS_DENIED;
   $shash{$sane_thread}{go} = 0;
   die; # quickest way out of the eval block
  }
  scan_page($device, $fh);
  $shash{$sane_thread}{status} = $Sane::STATUS+0;
  close $fh;
  printf "Scanned page %s.", $path if $debug;
  printf " (scanner status = %d)\n", $Sane::STATUS if $debug;
  $shash{$sane_thread}{status} = $Sane::STATUS+0;
  $shash{$sane_thread}{go} = 0;
 ';
 $shash{$sane_thread}{go} = 1;
 $shash{$sane_thread}{semaphore}->up;
 return;
}


sub scan_page {
 my ($device, $fh) = @_;
 my $first_frame = 1;
 my $offset = 0;
 my $must_buffer = 0;
 my $min = 0xff;
 my $max = 0;
 my %image;
 my @format_name = (
   "gray", "RGB", "red", "green", "blue"
 );
 my $total_bytes = 0;

 my $parm;
 {do { # extra braces to get last to work.
  if (!$first_frame) {
   $device->start;
   if ($Sane::STATUS != SANE_STATUS_GOOD) {
    printf "$prog_name: sane_start: $Sane::STATUS\n" if $debug;
    goto cleanup;
   }
  }

  $parm = $device->get_parameters;
  if ($Sane::STATUS != SANE_STATUS_GOOD) {
   printf "$prog_name: sane_get_parameters: $Sane::STATUS\n" if $debug;
   goto cleanup;
  }

  if ($debug) {
   if ($first_frame) {
    if ($parm->{lines} >= 0) {
     printf "$prog_name: scanning image of size %dx%d pixels at "
               ."%d bits/pixel\n",
               $parm->{pixels_per_line}, $parm->{lines},
               8 * $parm->{bytes_per_line} / $parm->{pixels_per_line};
    }
    else {
     printf "$prog_name: scanning image %d pixels wide and "
               ."variable height at %d bits/pixel\n",
               $parm->{pixels_per_line},
               8 * $parm->{bytes_per_line} / $parm->{pixels_per_line};
    }
   }

   printf "$prog_name: acquiring %s frame\n",
    $parm->{format} <= SANE_FRAME_BLUE ? $format_name[$parm->{format}]:"Unknown";
  }

  if ($first_frame) {
   if ($parm->{format} == SANE_FRAME_RED
       or $parm->{format} == SANE_FRAME_GREEN
       or $parm->{format} == SANE_FRAME_BLUE) {
    die unless ($parm->{depth} == 8);
    $must_buffer = 1;
    $offset = $parm->{format} - SANE_FRAME_RED;
   }
   elsif ($parm->{format} == SANE_FRAME_RGB) {
    die unless (($parm->{depth} == 8) || ($parm->{depth} == 16));
   }
   if ($parm->{format} == SANE_FRAME_RGB or $parm->{format} == SANE_FRAME_GRAY) {
    die unless (($parm->{depth} == 1) || ($parm->{depth} == 8)
            || ($parm->{depth} == 16));
    if ($parm->{lines} < 0) {
     $must_buffer = 1;
     $offset = 0;
    }
    else {
     write_pnm_header ($fh, $parm->{format}, $parm->{pixels_per_line},
                       $parm->{lines}, $parm->{depth});
    }
   }
  }
  else {
   die unless ($parm->{format} >= SANE_FRAME_RED
           && $parm->{format} <= SANE_FRAME_BLUE);
   $offset = $parm->{format} - SANE_FRAME_RED;
   $image{x} = $image{y} = 0;
  }
  my $hundred_percent = $parm->{bytes_per_line} * $parm->{lines}
    * (($parm->{format} == SANE_FRAME_RGB || $parm->{format} == SANE_FRAME_GRAY) ? 1:3);

  while (1) {
   my ($buffer, $len) = $device->read ($buffer_size);
   $total_bytes += $len;
   my $progr = $total_bytes/$hundred_percent;
   $progr = 1 if ($progr > 1);
   $shash{$sane_thread}{progress} = $progr;

   if ($Sane::STATUS != SANE_STATUS_GOOD) {
    printf "$prog_name: min/max graylevel value = %d/%d\n", $min, $max
     if ($debug and $parm->{depth} == 8);
    if ($debug and $Sane::STATUS != SANE_STATUS_EOF) {
     print "$prog_name: sane_read: $Sane::STATUS\n";
     return;
    }
    last;
   }

   if ($must_buffer) {
    # We're either scanning a multi-frame image or the
    # scanner doesn't know what the eventual image height
    # will be (common for hand-held scanners).  In either
    # case, we need to buffer all data before we can write
    # the image
    if ($parm->{format} == SANE_FRAME_RED
        or $parm->{format} == SANE_FRAME_GREEN
        or $parm->{format} == SANE_FRAME_BLUE) {
     for (my $i = 0; $i < $len; ++$i) {
      $image{data}[$offset + 3 * $i] = substr($buffer, $i, 1);
     }
     $offset += 3 * $len;
    }
    elsif ($parm->{format} == SANE_FRAME_RGB
           or $parm->{format} == SANE_FRAME_GRAY) {
     for (my $i = 0; $i < $len; ++$i) {
      $image{data}[$offset + $i] = substr($buffer, $i, 1);
     }
     $offset += $len;
    }
   }
   else { # ! must_buffer
     print $fh $buffer;
   }

   if ($debug && $parm->{depth} == 8) {
    for (split(//, $buffer)) {
     my $c = ord;
     if ($c >= $max) {
      $max = $c;
     }
     elsif ($c < $min) {
      $min = $c;
     }
    }
   }

  }
  $first_frame = 0;
 }
 while (!$parm->{last_frame});}

 if ($must_buffer) {
  if ($parm->{lines} > 0) {
   $image{height} = $parm->{lines};
  }
  else {
   $image{height} = @{$image{data}}/$parm->{pixels_per_line};
   $image{height} /= 3 if ($parm->{format} == SANE_FRAME_RED
                         or $parm->{format} == SANE_FRAME_GREEN
                         or $parm->{format} == SANE_FRAME_BLUE);
  }
  write_pnm_header ($fh, $parm->{format}, $parm->{pixels_per_line}, $image{height}, $parm->{depth});
  for (@{$image{data}}) {print $fh;}
 }

cleanup:
 my $expected_bytes = $parm->{bytes_per_line} * $parm->{lines} *
   (($parm->{format} == SANE_FRAME_RGB
     || $parm->{format} == SANE_FRAME_GRAY) ? 1 : 3);
 $expected_bytes = 0 if ($parm->{lines} < 0);
 if ($debug) {
  if ($total_bytes > $expected_bytes && $expected_bytes != 0) {
   printf "%s: WARNING: read more data than announced by backend "
         ."(%u/%u)\n", $prog_name, $total_bytes, $expected_bytes;
  }
  else {
   printf "%s: read %u bytes in total\n", $prog_name, $total_bytes;
  }
 }
 return;
}


sub write_pnm_header {
 my ($fh, $format, $width, $height, $depth) = @_;

# The netpbm-package does not define raw image data with maxval > 255.
# But writing maxval 65535 for 16bit data gives at least a chance
# to read the image.

 if ($format == SANE_FRAME_RED or $format == SANE_FRAME_GREEN or
                      $format == SANE_FRAME_BLUE or $format == SANE_FRAME_RGB) {
  printf $fh "P6\n# SANE data follows\n%d %d\n%d\n", $width, $height,
	      ($depth <= 8) ? 255 : 65535;
 }
 else {
  if ($depth == 1) {
   printf $fh "P4\n# SANE data follows\n%d %d\n", $width, $height;
  }
  else {
   printf $fh "P5\n# SANE data follows\n%d %d\n%d\n", $width, $height,
		($depth <= 8) ? 255 : 65535;
  }
 }
 return;
}


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


sub post_process_scan {
 my ($page, $rotate, $unpaper, $ocr) = @_;
 push @rotate_queue, [ $page, $rotate ] if $rotate;
 push @unpaper_stack, [ $page, options2unpaper($unpaper_options) ] if $unpaper;
 push @ocr_stack, $page if $ocr;
 print "OCRflagging page: $page\n" if $ocr && $debug;
 $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
 $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});

 if (not($rotating or $analyzing or $scanning or $running_unpaper or $running_ocr)) {
  if (@rotate_queue) {
   rotate();
  }
  elsif (@unpaper_stack) {
   unpaper_page();
  }
  elsif (@ocr_stack) {
   ocr_page();
  }
 }
 return;
}


# 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);
 return add_image($filename, $page, $resolution);
}


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 (! $resolution) {
  my $image = Image::Magick->new;
  my $x = $image->Read($ofilename);
  warn "$x" if "$x";
  $resolution = get_resolution($image);
 }

 my (undef, $filename) = tempfile(DIR => $SETTING{session}, 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 -> {row_changed_signal});
 push @{$slist -> {data}}, [ $page, 
                             get_pixbuf($filename, $heightt, $widtht),
                             $filename, undef, $resolution ];

# Block selection_changed_signal to prevent its firing changing pagerange to all
 $slist -> get_selection -> signal_handler_block($slist -> {selection_changed_signal});
 $slist -> get_selection -> unselect_all;
 manual_sort_by_column ($slist, 0);
 $slist -> get_selection -> signal_handler_unblock($slist -> {selection_changed_signal});
 $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});

 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[0];
}


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


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


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


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


# 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;
   $suffix = $1 if ($_->[2] =~ /(\.\w*)$/);
   my (undef, $new) = tempfile(DIR => $SETTING{session}, 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 -> {row_changed_signal});

  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 -> {row_changed_signal});

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


# Delete the selected scans

sub delete_pages {

# Update undo/redo buffers
 take_snapshot();

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

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

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

# Select nothing
 elsif (@{$slist->{data}}) {
  $slist->select;
 }

# No pages left, and having blocked the selection_changed_signal, we've got to clear the image
 else {
  $view -> set_pixbuf(undef);
  $textview -> set_sensitive(FALSE);
  $textbuffer -> set_text('');
 }

# Reset start page in scan dialog
 reset_start();

 update_uimanager();
 return;
}


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


# Select all scans

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


# Select all odd(0) or even(1) scans

sub select_odd_even {
 my $odd = shift;
 my @selection;
 for (0 .. $#{$slist -> {data}}) {
  push @selection, $_ if ($slist -> {data}[$_][0] % 2 xor $odd);
 }
 
 $slist -> get_selection -> unselect_all;
 $slist -> select(@selection);
 return;
}


sub select_modified_since_ocr {
 my @selection;
 foreach my $page (0..$#{$slist -> {data}} ) {
  my $dirty_time = $slist -> {data}[$page][8];
  my $ocr_flag = $slist -> {data}[$page][9];
  my $ocr_time = $slist -> {data}[$page][10];
  $dirty_time = defined($dirty_time)?$dirty_time:0;
  $ocr_time = defined($ocr_time)?$ocr_time:0;
  push @selection, $_ if ($ocr_flag and ($ocr_time le $dirty_time));
 }

 $slist -> get_selection -> unselect_all;
 $slist -> select(@selection);
 return;
}


# Analyze and select blank pages

sub analyze_select_blank {
 update_analysis(1,0);
 return;
}


# Select blank pages

sub select_blank_pages {
 $slist -> get_selection -> unselect_all;
 foreach my $page (0..$#{$slist -> {data}} ) {
  print "smp page: $page\n" if $debug;
  my $dirty_time = $slist -> {data}[$page][8];
  my $analyze_time = $slist -> {data}[$page][11];
  $dirty_time = defined($dirty_time)?$dirty_time:0;
  $analyze_time = defined($analyze_time)?$analyze_time:0;
  if ($analyze_time le $dirty_time) {
   warn $d->get(sprintf("Page %d probably needs to be re-Analyzed.  Try Update or Analyze.\n",$page+1));
   next;
  }
  #compare Std Dev to threshold
  if ($slist -> {data}[$page][7] <= $SETTING{'Blank threshold'} ) { 
   $slist -> select($page);
   print "selecting blank page\n" if $debug;
  }
  print "StdDev: ".$slist->{data}[$page][7]." threshold: ".$SETTING{'Blank threshold'}."\n" if $debug;
 }
 return;
}


# Analyze and select dark pages

sub analyze_select_dark {
 update_analysis(0,1);
 return;
}


# Select dark pages

sub select_dark_pages {
 $slist -> get_selection -> unselect_all;
 print "checking for dark pages now, is analysis done?\n" if $debug;
 foreach my $page (0..$#{$slist -> {data}} ) {
  print "smp page: $page\n" if $debug;
  my $dirty_time = $slist -> {data}[$page][8];
  my $analyze_time = $slist -> {data}[$page][11];
  $dirty_time = defined($dirty_time)?$dirty_time:0;
  $analyze_time = defined($analyze_time)?$analyze_time:0;
  if ($analyze_time le $dirty_time) {
   warn $d->get(sprintf("Page %d probably needs to be re-Analyzed.  Try Update or Analyze.\n",$page+1));
   next;
  }
  #compare Mean to threshold
  if ($slist -> {data}[$page][6] <= $SETTING{'Dark threshold'}) {
   $slist -> select($page);
   print "selecting dark page\n" if $debug;
  }
 }
 return;
}


# Update analysis on stale pages

sub update_analysis {
 my ($select_blank, $select_dark) = @_;
 foreach my $page (0..$#{$slist -> {data}} ) {
  my $dirty_time = $slist -> {data}[$page][8];
  my $analyze_time = $slist -> {data}[$page][11];
  $dirty_time = defined($dirty_time)?$dirty_time:0;
  $analyze_time = defined($analyze_time)?$analyze_time:0;
  if ($analyze_time le $dirty_time) {
   print "updating: $page analyse_time: $analyze_time dirty_time: $dirty_time\n" if $debug;
   push @analyze_queue, [$page];
  }
 }
 analyze($select_blank,$select_dark) if @analyze_queue;
 select_blank_pages() if ($select_blank and not @analyze_queue);
 select_dark_pages() if ($select_dark and not @analyze_queue);
 return;
}

# Update OCR on stale pages

sub update_ocr {
 foreach my $page (0..$#{$slist -> {data}} ) {
  my $dirty_time = $slist -> {data}[$page][8];
  my $ocr_flag = $slist -> {data}[$page][9];
  my $ocr_time = $slist -> {data}[$page][10];
  $dirty_time = defined($dirty_time)?$dirty_time:0;
  $ocr_time = defined($ocr_time)?$ocr_time:0;
  if ($ocr_flag and ($ocr_time le $dirty_time) ) {
   print "ocr_flag: $ocr_flag ocr_time: $ocr_time dirty_time: $dirty_time\n" if $debug;
   push @ocr_stack, $page;
  }
 }
 ocr_page() if @ocr_stack;
 return;
}

# 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_name ($prog_name);
 $about->set_version ($version);
 my $authors = <<EOS;
John Goerzen
Chris Mayo
David Hampton
Sascha Hunold
Jakub Wilk
freedo
Roy Shahbazian
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--2009 Jeffrey Ratcliffe'));
 my $licence = <<EOS;
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright (C) 2006 -- 2009  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;
You Hyun Jo
Manuel Rennecke
Mikal Krogstad
Robert Hrovat
rmare
Jacob Nielsen
Mattias Ohlsson
Dirk Tas
albertotomaduz
starmaker
Serhey Kusyumoff (Сергій Кусюмов)
Jan Klopper
Emil Pavlov
Poppe
saabaero
Alexandre Prokoudine
Matthias Gutjahr
Christoph Langner
Szenográdi Norbert Péter
EOS
 $about->set_translator_credits ($translators);
 $about->set_artists ('lodp');
 $about->run;
 $about->destroy;
 return;
}


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

# Get the options from the cache if defined
 my $mode;
 if (defined $SETTING{mode}) {
  $mode = $SETTING{mode};
 }
 else {
  $mode = 'default';
 }
 if ($SETTING{'cache options'} and defined $SETTING{cache}{$device}{$mode}) {
  parse_options($vboxd, $device, options2hash($SETTING{cache}{$device}{$mode}));
  return;
 }

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

# Get output from scanimage or scanadf.
# Inverted commas needed for strange characters in device name
  my $cmd = "$SETTING{'scan prefix'} $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 (not 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, 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 mode
  my $filename = $test->{file}[$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, options2hash($output));
 }
 return;
}


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

# 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 = $hash{x}{max} if (defined $hash{x}{max});
 $y = $hash{y}{max} if (defined $hash{y}{max});
 $l = $hash{l}{max} if (defined $hash{l}{max});
 $t = $hash{t}{max} if (defined $hash{t}{max});

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

 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 ($ddo{'Paper size'}{string});
  $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);
 }

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

   if ($option =~ /^page-?width$/) {
    $w = $hash{$option}{max};
    $spin_buttonw = $widget;
    $vboxm -> pack_start ($hbox, TRUE, TRUE, 0);
   }
   elsif ($option =~ /^page-?height$/) {
    $h = $hash{$option}{max};
    $spin_buttonh = $widget;
    $vboxm -> pack_start ($hbox, TRUE, TRUE, 0);
   }
   else {
    $vboxd -> pack_start ($hbox, TRUE, TRUE, 0);
   }
   $batch_scan = $widget if ($option =~ /^batch-scan$/);

# 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)/i) {
      $bscanall->set_sensitive(FALSE);
      $bscannum->set_active(TRUE);
     }
     else {
      $bscanall->set_sensitive(TRUE);
      if (defined($text) and
       get_value(\%ddo, $option, $text) =~ /(ADF|Automatic Document Feeder)/i) {
       $batch_scan -> set_active(0) if (defined $batch_scan);
      }
     }
     if (defined($text) and get_value(\%ddo, $option, $text) =~ /duplex/i) {
      $duplex = TRUE;
      $frames->set_sensitive(FALSE);
     }
     else {
      $duplex = FALSE;
      $frames->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});
    }
    Glib::Idle->add (sub { $vboxm -> hide; });
   }
  });
  add_paper($combobp, $x, $y, $l, $t, $h, $w);
 }

# Show window
 $vboxd -> show_all;
# Give the GUI a chance to catch up before resizing.
 Glib::Idle->add (sub { $windows -> resize(100, 100); });
 set_paper($combobp);

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


# 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);
  $SETTING{$_->{letter}} = $_->{dimension}
   if (! defined($SETTING{$_->{letter}}));
  $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;
 return;
}


# 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) = @_;
 return if (! defined $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) if (defined $combobox);
 return;
}


# 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 $mode;
 if (defined $SETTING{mode}) {
  $mode = $SETTING{mode};
 }
 else {
  $mode = 'default';
 }
 if ($SETTING{'cache options'} and defined $SETTING{cache}{$device}{$mode}) {
  update_options_hash($vboxd, options2hash($SETTING{cache}{$device}{$mode}));
  return;
 }

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

# Get output from scanimage or scanadf.
# Inverted commas needed for strange characters in device name
  my $cmd = "$SETTING{'scan prefix'} $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 (not 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->{file}[$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;
}


# return a hash of the passed options

sub options2hash {

 my ($output) = @_;
 my %hash = Gscan2pdf::options2hash($output);
 warn 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{$_};
  }
 }

# Cache options
 my $mode;
 if (defined $SETTING{mode}) {
  $mode = $SETTING{mode};
 }
 else {
  $mode = 'default';
 }
# We could cache the hash, but we'd have to turn off MergeDuplicateOptions
 $SETTING{cache}{$SETTING{device}}{$mode} = $output
  if ($SETTING{'cache options'} and defined $SETTING{device});

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


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


{
 my $start = 1;
 my $step = 1;
 my ($spin_buttons, $spin_buttoni);


# Scan device-dependent scan options

sub scan_options {
 my ($notebook, $device) = @_;

# Remove any existing pages
 while ($notebook->get_n_pages > 1) {
  $notebook->remove_page (-1);
 }

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

# Set up ProgressBar
 my $dialog = Gtk2::Dialog -> new ($d->get('Updating options')."...", $windows2,
                                           'modal',
                                           'gtk-cancel' => 'cancel');
 my $pbar = Gtk2::ProgressBar->new;
 $pbar -> set_pulse_step(.1);
 $pbar->set_text ($d->get('Updating options'));
 $dialog -> vbox -> add($pbar);

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

 $shash{$sane_thread}{device} = $device->{name};
 $shash{$sane_thread}{process} = '
  undef $device if (defined($device)); # close the handle

# open the device in the sane thread
  $device = Sane::Device->open($shash{$sane_thread}{device});

  if ($Sane::STATUS != SANE_STATUS_GOOD) {
   warn "Error opening device: $Sane::STATUS\n";
   return;
  }

  my @options;

# We got a device, find out how many options it has:
  my $num_dev_options = $device->get_option(0);
  if ($Sane::STATUS != SANE_STATUS_GOOD) {
   warn "Error: unable to determine option count\n";
   return;
  }
  for (my $i = 1; $i < $num_dev_options; ++$i) {
   my $opt = $device->get_option_descriptor ($i);
   $options[$i] = $opt;
   next if (! ($opt->{cap} & SANE_CAP_SOFT_DETECT));

   $opt->{val} = $device->get_option ($i) if ($opt->{type} != SANE_TYPE_BUTTON);
  }
  $shash{$sane_thread}{data} = freeze(\@options);
  $shash{$sane_thread}{go} = 0;
 ';
 $shash{$sane_thread}{go} = 1;
 $shash{$sane_thread}{semaphore}->up;

# Timer will run until callback returns false 
 my $timer = Glib::Timeout->add (100, sub {
  if ($shash{$sane_thread}{go}) {
   $pbar->pulse;
   return TRUE;
  }
  else {
   my @options = @{thaw($shash{$sane_thread}{data})};
   print "Sane->get_option_descriptor returned: ", Dumper(\@options) if $debug;

   my ($group, $vbox, @widgets, $hboxp, %optnamei);
   my $num_dev_options = $#options+1;
   my %geometry;
   undef $combobp; # So we don't carry over from one device to another
   for (my $i = 1; $i < $num_dev_options; ++$i) {
    my $opt = $options[$i];

# Notebook page for group
    if ($opt->{type} == SANE_TYPE_GROUP or not defined($vbox)) {
     $vbox = Gtk2::VBox->new;
     $group = $opt->{type} == SANE_TYPE_GROUP ? $d_sane->get($opt->{title}) : $d->get('Scan Options');
     $notebook->append_page($vbox, $group);
     next;
    }

    next if (! ($opt->{cap} & SANE_CAP_SOFT_DETECT));

# Worry about the multiple value options later
if ($opt->{max_values} < 2) {

    if (($opt->{type} == SANE_TYPE_FIXED or $opt->{type} == SANE_TYPE_INT)
       and ($opt->{unit} == SANE_UNIT_MM or $opt->{unit} == SANE_UNIT_PIXEL)
       and (($opt->{name} eq SANE_NAME_SCAN_TL_X) or 
            ($opt->{name} eq SANE_NAME_SCAN_TL_Y) or
            ($opt->{name} eq SANE_NAME_SCAN_BR_X) or
            ($opt->{name} eq SANE_NAME_SCAN_BR_Y))) {
     $geometry{$opt->{name}}{i} = $i;

# Define HBox for paper size here
# so that it can be put before first geometry option
     if (not defined($hboxp)) {
      $hboxp = Gtk2::HBox -> new;
      $vbox -> pack_start ($hboxp, FALSE, FALSE, 0);
     }
    }

# HBox for option
    my $hbox = Gtk2::HBox -> new;
    $vbox -> pack_start ($hbox, FALSE, TRUE, 0);
    $hbox->set_sensitive(FALSE)
     if ($opt->{cap} & SANE_CAP_INACTIVE or not $opt->{cap} & SANE_CAP_SOFT_SELECT);

# Label
    if ($opt->{type} != SANE_TYPE_BUTTON) {
     my $label = Gtk2::Label -> new ($d_sane->get($opt->{title}));
     $hbox -> pack_start ($label, FALSE, FALSE, 0);
    }

# Widget
    my ($widget, $val);
    $val = $opt->{val};

# CheckButton
    if ($opt->{type} == SANE_TYPE_BOOL) {
     $widget = Gtk2::CheckButton -> new;
     $widget->set_active(TRUE) if ($val);
     $widget -> {signal} = $widget -> signal_connect (toggled => sub {
      my $val = $widget -> get_active;
      set_option ($widget->{option_number}, $val, @widgets);
     });
    }

# Button
    elsif ($opt->{type} == SANE_TYPE_BUTTON) {
     $widget = Gtk2::Button -> new($d_sane->get($opt->{title}));
     $widget -> {signal} = $widget -> signal_connect (clicked => sub {
      set_option ($widget->{option_number}, $val, @widgets);
     });
    }

# SpinButton
    elsif ($opt->{constraint_type} == SANE_CONSTRAINT_RANGE) {
     my $step = 1;
     $step = $opt->{constraint}{quant} if ($opt->{constraint}{quant});
     $widget = Gtk2::SpinButton -> new_with_range($opt->{constraint}{min},
                                                  $opt->{constraint}{max}, $step);

# Set the default
     $widget -> set_value($val)
      if (defined $val and not $opt->{cap} & SANE_CAP_INACTIVE);
     $widget -> {signal} = $widget -> signal_connect ('value-changed' => sub {
      my $val = $widget -> get_value;
      set_option ($widget->{option_number}, $val, @widgets);
     });

# Note the max if geometry
     $geometry{$opt->{name}}{max} = $opt->{constraint}{max}
      if (defined($geometry{$opt->{name}}));
    }

# ComboBox
    elsif ($opt->{constraint_type} == SANE_CONSTRAINT_STRING_LIST
                      or $opt->{constraint_type} == SANE_CONSTRAINT_WORD_LIST) {
     $widget = Gtk2::ComboBox -> new_text;
     my $index = 0;
     for (my $i = 0; $i < @{$opt->{constraint}}; ++$i) {
      $widget->append_text($d_sane->get($opt->{constraint}[$i]));
      $index = $i if (defined $val and $opt->{constraint}[$i] eq $val);
     }

# Set the default
     $widget -> set_active($index) if (defined $index);
     $widget -> {signal} = $widget -> signal_connect (changed => sub {
      my $i = $widget->get_active;
      set_option ($widget->{option_number}, $opt->{constraint}[$i], @widgets);
     });
    }

# Entry
    elsif ($opt->{constraint_type} == SANE_CONSTRAINT_NONE) {
     $widget = Gtk2::Entry -> new;

# Set the default
     $widget -> set_text($val)
      if (defined $val and not $opt->{cap} & SANE_CAP_INACTIVE);
     $widget -> {signal} = $widget->signal_connect (activate => sub {
      my $val = $widget -> get_text;
      set_option ($widget->{option_number}, $val, @widgets);
     });
    }

if (defined $widget) {
    $widget->{option_number} = $i;
    $widget->{option_name} = $opt->{name};
    $optnamei{$opt->{name}} = $widget;
    $widgets[$i] = $widget;
    if ($opt->{type} == SANE_TYPE_BUTTON) {
     $hbox -> pack_end ($widget, TRUE, TRUE, 0);
    }
    else {
     $hbox -> pack_end ($widget, FALSE, FALSE, 0);
    }
    $tooltips -> set_tip ($widget, $d_sane->get($opt->{desc}));

# Look-up to hide/show the box if necessary
    $geometry{$opt->{name}}{box} = $hbox
     if (defined($geometry{$opt->{name}}));

    if (defined($geometry{scalar(SANE_NAME_SCAN_BR_X)})
        and defined($geometry{scalar(SANE_NAME_SCAN_BR_Y)})
        and defined($geometry{scalar(SANE_NAME_SCAN_TL_X)})
        and defined($geometry{scalar(SANE_NAME_SCAN_TL_Y)})
        and not defined($combobp)) {

# Paper list
     my $label = Gtk2::Label -> new ($d->get('Paper size'));
     $hboxp -> pack_start ($label, 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);
     $combobp -> signal_connect (changed => sub {
      if ($combobp -> get_active_text eq $d->get('Edit')) {
       edit_paper($combobp, $geometry{scalar(SANE_NAME_SCAN_BR_X)}{max},
                            $geometry{scalar(SANE_NAME_SCAN_BR_Y)}{max},
                            $geometry{scalar(SANE_NAME_SCAN_TL_X)}{max},
#                            $geometry{scalar(SANE_NAME_SCAN_TL_Y)}{max}, $h, $w);
                            $geometry{scalar(SANE_NAME_SCAN_TL_Y)}{max});
      }
      elsif ($combobp -> get_active_text eq $d->get('Manual')) {
       for (( SANE_NAME_SCAN_TL_X, SANE_NAME_SCAN_TL_Y,
              SANE_NAME_SCAN_BR_X, SANE_NAME_SCAN_BR_Y )) {
        $geometry{$_}{box}->show_all if (defined $geometry{$_});
       }
      }
      else {
       my $paper = $combobp -> get_active_text;
       $widgets[$geometry{scalar(SANE_NAME_SCAN_TL_X)}{i}]->set_value(
        $SETTING{Paper}{$paper}{l}
       ) if (defined $geometry{scalar(SANE_NAME_SCAN_TL_X)});
       $widgets[$geometry{scalar(SANE_NAME_SCAN_TL_Y)}{i}]->set_value(
        $SETTING{Paper}{$paper}{t}
       ) if (defined $geometry{scalar(SANE_NAME_SCAN_TL_Y)});
       $widgets[$geometry{scalar(SANE_NAME_SCAN_BR_X)}{i}]->set_value(
        $SETTING{Paper}{$paper}{x}+$SETTING{Paper}{$paper}{l}
       );
       $widgets[$geometry{scalar(SANE_NAME_SCAN_BR_Y)}{i}]->set_value(
        $SETTING{Paper}{$paper}{y}+$SETTING{Paper}{$paper}{t}
       );
#       if (defined($spin_buttonh) and defined($spin_buttonw)) {
#        $spin_buttonw -> set_value($SETTING{Paper}{$paper}{x});
#        $spin_buttonh -> set_value($SETTING{Paper}{$paper}{y});
#       }
       Glib::Idle->add (sub {
        for (( SANE_NAME_SCAN_TL_X, SANE_NAME_SCAN_TL_Y,
               SANE_NAME_SCAN_BR_X, SANE_NAME_SCAN_BR_Y )) {
         $geometry{$_}{box}->hide_all if (defined $geometry{$_});
        }
       });
      }
     });
     add_paper($combobp, $geometry{scalar(SANE_NAME_SCAN_BR_X)}{max},
                         $geometry{scalar(SANE_NAME_SCAN_BR_Y)}{max},
                         $geometry{scalar(SANE_NAME_SCAN_TL_X)}{max},
#                         $geometry{scalar(SANE_NAME_SCAN_TL_Y)}{max}, $h, $w);
                         $geometry{scalar(SANE_NAME_SCAN_TL_Y)}{max});
    }

}
else {
print "Unknown type $opt->{type}\n";
}
}
   }

# Set defaults
# Move them first to a dummy array, as otherwise it would be self-modifying
   if (defined $SETTING{default}{$shash{$sane_thread}{device}}) {
# Config::General flattens arrays with 1 entry to scalars,
# so we must check for this
    my @defaults;
    if (ref($SETTING{default}{$shash{$sane_thread}{device}}) ne 'ARRAY') {
     push @defaults, $SETTING{default}{$shash{$sane_thread}{device}};
    }
    else {
     @defaults = @{$SETTING{default}{$shash{$sane_thread}{device}}};
    }
    delete $SETTING{default}{$shash{$sane_thread}{device}};

# Give the GUI a chance to catch up between settings,
# in case they have to be reloaded.
# Can't do this in Glib::Idle->add as the GUI is also idle waiting for the
# sane thread to return, so manually flagging each loop
    my $i = 0;
# Timer will run until callback returns false 
    my $timer = Glib::Timeout->add (100, sub {
     return FALSE unless ($i < @defaults);
     unless ($gui_updating) {
      $gui_updating = TRUE;
      my ($option, $val) = each(%{$defaults[$i]});
      my $widget = $optnamei{$option};
      if ($widget->isa('Gtk2::CheckButton')) {
       if ($widget->get_active != $val) {
        $widget->set_active($val);
       }
       else {
        $gui_updating = FALSE;
       }
      }
      elsif ($widget->isa('Gtk2::SpinButton')) {
       if ($widget->get_value != $val) {
        $widget->set_value($val);
       }
       else {
        $gui_updating = FALSE;
       }
      }
      elsif ($widget->isa('Gtk2::ComboBox')) {
       my $opt = $options[$widget->{option_number}];
       if ($opt->{constraint}[$widget->get_active] ne $val) {
        my $index;
        for (my $i = 0; $i < @{$opt->{constraint}}; ++$i) {
         $index = $i if ($opt->{constraint}[$i] eq $val);
        }
        $widget -> set_active($index) if (defined $index);
       }
       else {
        $gui_updating = FALSE;
       }
      }
      elsif ($widget->isa('Gtk2::Entry')) {
       if ($widget->get_text ne $val) {
        $widget -> set_text($val);
       }
       else {
        $gui_updating = FALSE;
       }
      }
      $i++;
     }
     return TRUE;
    });
   }

# Show new pages
   for (my $i = 1; $i < $notebook->get_n_pages; $i++) {
    $notebook->get_nth_page ($i)->show_all;
   }

# Give the GUI a chance to catch up before resizing.
   Glib::Idle->add (sub { $windows2 -> resize(100, 100); });
   set_paper($combobp);

   $sbutton->set_sensitive(TRUE);
   $dialog -> destroy;
  }
 });

 return;
}


# Update the sane option in the thread
# If necessary, reload the options,
# and walking the widget tree, update the widgets

sub set_option {
 my ($i, $val, @widgets) = @_;

# Cache option
 push @{$SETTING{default}{$shash{$sane_thread}{device}}},
                                          {$widgets[$i]->{option_name} => $val};

# Note any duplicate options, keeping only the last entry.
 my %seen;
 my $j = $#{$SETTING{default}{$shash{$sane_thread}{device}}};
 while ($j > -1) {
  my ($option) = keys(%{$SETTING{default}{$shash{$sane_thread}{device}}[$j]});
  $seen{$option}++;
  if ($seen{$option} > 1) {
   splice @{$SETTING{default}{$shash{$sane_thread}{device}}}, $j, 1;
  }
  $j--;
 }

# Set up ProgressBar
 my $dialog = Gtk2::Dialog -> new ($d->get('Updating options')."...", $windows2,
                                           'modal',
                                           'gtk-cancel' => 'cancel');
 my $pbar = Gtk2::ProgressBar->new;
 $pbar -> set_pulse_step(.1);
 $pbar->set_text ($d->get('Updating options'));
 $dialog -> vbox -> add($pbar);

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

 {
  my @data = ($i, $val);
  lock($shash{$sane_thread}{process});
  $shash{$sane_thread}{data} = freeze(\@data);
  $shash{$sane_thread}{process} = '
   my ($i, $val) = @{thaw($shash{$sane_thread}{data})};
   undef $shash{$sane_thread}{data};
   my $info = $device->set_option ($i, $val);
   if ($info & SANE_INFO_RELOAD_OPTIONS) {
    my @options;

# We got a device, find out how many options it has:
    my $num_dev_options = $device->get_option(0);
    if ($Sane::STATUS != SANE_STATUS_GOOD) {
     warn "Error: unable to determine option count\n";
     return;
    }
    for (my $i = 1; $i < $num_dev_options; ++$i) {
     my $opt = $device->get_option_descriptor ($i);
     $options[$i] = $opt;
     next if (! ($opt->{cap} & SANE_CAP_SOFT_DETECT));

     $opt->{val} = $device->get_option ($i) if ($opt->{type} != SANE_TYPE_BUTTON);
    }
    $shash{$sane_thread}{data} = freeze(\@options);
   }
   $shash{$sane_thread}{go} = 0;
  ';
  $shash{$sane_thread}{go} = 1;
  $shash{$sane_thread}{semaphore}->up;
 }

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

# walk the widget tree and update them from the hash
    my @options = @{thaw($shash{$sane_thread}{data})};
    print "Sane->get_option_descriptor returned: ", Dumper(\@options) if $debug;

    my ($group, $vbox);
    my $num_dev_options = $#options+1;
    for (my $i = 1; $i < $num_dev_options; ++$i) {
     my $widget = $widgets[$i];
  
     if (defined $widget) { # could be undefined for !($opt->{cap} & SANE_CAP_SOFT_DETECT)
      my $opt = $options[$i];
      my $val = $opt->{val};
      $widget -> signal_handler_block($widget -> {signal});

# Worry about the multiple value options later
if ($opt->{max_values} < 2) {

# HBox for option
      my $hbox = $widget->parent;
      $hbox->set_sensitive((not $opt->{cap} & SANE_CAP_INACTIVE)
                            and $opt->{cap} & SANE_CAP_SOFT_SELECT);

# CheckButton
      if ($opt->{type} == SANE_TYPE_BOOL) {
       $widget->set_active($val)
        if (defined $val and not $opt->{cap} & SANE_CAP_INACTIVE);
      }

# SpinButton
      elsif ($opt->{constraint_type} == SANE_CONSTRAINT_RANGE) {
       my ($step, $page) = $widget->get_increments;
       $step = 1;
       $step = $opt->{constraint}{quant} if ($opt->{constraint}{quant});
       $widget->set_range($opt->{constraint}{min}, $opt->{constraint}{max});
       $widget->set_increments($step, $page);
       $widget -> set_value($val)
        if (defined $val and not $opt->{cap} & SANE_CAP_INACTIVE);
      }

# ComboBox
      elsif ($opt->{constraint_type} == SANE_CONSTRAINT_STRING_LIST
                          or $opt->{constraint_type} == SANE_CONSTRAINT_WORD_LIST) {
       $widget->get_model->clear;
       my $index = 0;
       for (my $i = 0; $i < @{$opt->{constraint}}; ++$i) {
        $widget->append_text($d_sane->get($opt->{constraint}[$i]));
        $index = $i if (defined $val and $opt->{constraint}[$i] eq $val);
       }
       $widget -> set_active($index) if (defined $index);
      }

# Entry
      elsif ($opt->{constraint_type} == SANE_CONSTRAINT_NONE) {
       $widget -> set_text($val)
        if (defined $val and not $opt->{cap} & SANE_CAP_INACTIVE);
      }

}
      $widget -> signal_handler_unblock($widget -> {signal});
     }
    }
   }
   $dialog -> destroy;
   $gui_updating = FALSE; # We can carry on applying defaults now, if necessary.
  }
 });
 return;
}


# Dialog for renumber

sub renumber_dialog {
 if (defined $windowrn) {
  $windowrn -> present;
  return;
 }

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

# Frame for page range
 my $frame = Gtk2::Frame->new($d->get('Page Range'));
 $vbox -> pack_start ($frame, FALSE, FALSE, 0);
 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;
  update_renumber() if ($SETTING{'Page range'} eq 'selected');
 });
 $frame->add ($pr);
 push @prlist, $pr;

# Frame for page numbering
 my $framex = Gtk2::Frame -> new($d->get('Page numbering'));
 $vbox -> 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);
 $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);
 $spin_buttoni -> set_value($step);
 $hboxi -> pack_end($spin_buttoni, FALSE, FALSE, 0);

# Check whether the settings are possible
 $spin_buttoni -> signal_connect ('value-changed' => \&update_renumber);
 $spin_buttons -> signal_connect ('value-changed' => \&update_renumber);
 update_renumber();

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

# Start button
 my $obutton = Gtk2::Button -> new($d->get('Renumber'));
 $hbox -> pack_start( $obutton, TRUE, TRUE, 0 );
 $obutton -> signal_connect( clicked => sub {
  my ($start_new, $step_new, $current, $not_selected, $n) = build_lists();
  if ($n > -1 and
              ($current->min < 1 or $current->intersect($not_selected)->size)) {
   show_message_dialog($windowo, 'error', 'close', $d->get('The current settings would result in duplicate page numbers. Please select new start and increment values.'));
  }
  else {

# Update undo/redo buffers
   take_snapshot();
   $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
   renumber($slist, 0, $start_new, $step_new, $SETTING{'Page range'});

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

# Convert to page numbers
   for (@page) {
    $_ = $slist -> {data}[$_][0];
   }

# Block selection_changed_signal to prevent its firing changing pagerange to all
   $slist -> get_selection -> signal_handler_block($slist -> {selection_changed_signal});

# Select new page, deselecting others. This fires the select callback,
# displaying the page
   $slist -> get_selection -> unselect_all;
   manual_sort_by_column ($slist, 0);
   $slist -> get_selection -> signal_handler_unblock($slist -> {selection_changed_signal});
   $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});

# Convert back to indices
   for (@page) {

# Due to the sort, must search for new page
    my $page = 0;
    ++$page while ($page < $#{$slist -> {data}}
                                    and $slist -> {data}[$page][0] != $_);
    $_ = $page;
   }

# Reselect pages
   $slist -> select(@page);
  }
 });

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

 $windowrn -> show_all;
 return;
}


# Helper function to prevent impossible settings in renumber dialog

sub update_renumber {
 if ($SETTING{'Page range'} eq 'selected') {
  my ($start_new, $step_new, $current, $not_selected, $n) = build_lists();

  my $dstart = $start_new - $start;
  my $dstep = $step_new - $step;
  $dstart = 1 if ($dstart == 0 and $dstep == 0);

# Check for clash with non_selected
  while ($n > -1 and
              ($current->min < 1 or $current->intersect($not_selected)->size)) {
   if ($current->min < 1) {
    if ($dstart < 0) {
     $dstart = 1;
    }
    else {
     $dstep = 1;
    }
   }
   $start_new += $dstart;
   $step_new += $dstep;
   $step_new += $dstep if ($step_new == 0);
   $current->copy;
   $current->insert($start_new+$step_new*$_) for (0..$n);
  }

  $spin_buttons -> set_value($start_new);
  $spin_buttoni -> set_value($step_new);
  $start = $start_new;
  $step = $step_new;
 }
 return;
}


# Helper function to build page lists

sub build_lists {

# Get list of pages not in selection
 my @selected = get_page_index();
 my @all = (0..$#{$slist -> {data}});

# Convert the indices to sets of page numbers
 @selected = index2page_number(@selected);
 @all = index2page_number(@all);
 my $selected = Set::IntSpan->new(\@selected);
 my $all = Set::IntSpan->new(\@all);
 my $not_selected = $all->diff($selected);

# Create a set from the current settings
 my $start_new = $spin_buttons->get_value;
 my $step_new = $spin_buttoni->get_value;
 my $dstart = $start_new - $start;
 my $dstep = $step_new - $step;
 $dstart = 1 if ($dstart == 0 and $dstep == 0);
 $step_new += $dstep if ($step_new == 0);
 my $current = Set::IntSpan->new;
 $current->insert($start_new+$step_new*$_) for (0..$#selected);

 return ($start_new, $step_new, $current, $not_selected, $#selected);
}

}


# helper function to return an array of page numbers given an array of page indices

sub index2page_number {
 my @index = @_;
 for (@index) {
  $_ = ${$slist -> {data}}[$_][0];
 }
 return @index;
}


# Renumber pages

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

 if (defined($start)) {
  $step = 1 if (! defined($step));
  $selection = 'all' if (! defined($selection));

  my @selection;
  if ($selection eq 'selected') {
   @selection = $slist -> get_selected_indices;
  }
  else {
   @selection =  0..$#{$slist -> {data}};
  }

  for (@selection) {
   $slist -> {data}[$_][$column] = $start;
   $start += $step;
  }
 }

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


# Compute a timestamp

sub timestamp {
 my @time=localtime();
 # return a time which can be string-wise compared
 return sprintf ("%04d%02d%02d%02d%02d%02d",$time[5],$time[4],$time[3],$time[2],$time[1],$time[0]);
}

# Rotate selected images

sub rotate_selected {
 my ($degrees) = @_;
 my @page = $slist -> get_selected_indices;
 my @list;
 push @rotate_queue, [ $_, $degrees ] foreach (@page);

# Update undo/redo buffers
 take_snapshot();

 rotate();
 return;
}


# Rotate images in @rotate_queue ($index, $degrees)

sub rotate {
 my $page = 0;
 my $npages = 0;

# guess where unpaper has been called from
 my $window = get_parent($windows, $window);
 my $dialog = Gtk2::Dialog -> new ($d->get('Rotating')."...", $window,
                                           'modal',
                                           '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;
  $rotating = FALSE;
  kill_subs();
 });
 $dialog -> show_all;

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

 my ($child, $parent) = open_socketpair();
 my $pid = start_process(sub {
  send($parent, 'ready', 0);

  while (TRUE) {

# Now block until the GUI passes the filename and direction
   my $rin = '';
   my $rout = '';
   my ($index, $filename, $degrees);
   vec($rin, $parent->fileno(), 1) = 1;
   if (select($rout=$rin,undef,undef,undef)) {
    my $line;
    recv($parent, $line, 1000, 0);
    return if ($line eq 'finished');
    ($index, $filename, $degrees) = split ' ', $line;
   }

# Rotate with imagemagick
   my $image = Image::Magick->new;
   my $x = $image->Read($filename);
   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";
   my $suffix;
   $suffix = $1 if ($filename =~ /\.(\w*)$/);
   (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => '.'.$suffix);
   $x = $image->Write(filename => $filename, depth => $depth);
   warn "$x" if "$x";
   send($parent, "$index $filename", 0);
  }
 });

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($child, $line, 1000, 0);
   my ($index, $filename) = split ' ', $line;
   if (defined $filename and $filename ne '' and $index ne 'ready') {
    $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
    $slist -> {data}[$index][2] = $filename;
    $slist -> {data}[$index][1] = get_pixbuf($filename, $heightt, $widtht);
    $slist -> {data}[$index][8] = timestamp(); #flag as dirty
    $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
    my @selected = $slist -> get_selected_indices;
    display_image($filename) if ($index == $selected[0]);
   }
   if (@rotate_queue) {
    $page++;
    $npages++;
    my ($index, $degrees) = @{shift @rotate_queue};
    my $filename = $slist -> {data}[$index][2];
    send($child, "$index $filename $degrees", 0);
    $pbar->set_text(sprintf($d->get("Rotating page %i of %i"), $page, $#rotate_queue+$npages+1));
    $pbar->set_fraction (($page-1)/($#rotate_queue+$npages+1));
   }
   else {
    send($child, 'finished', 0);
    $dialog -> destroy;
    $rotating = FALSE;
    post_process_scan();
    return FALSE;  # uninstall
   }
  }

# 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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   $rotating = FALSE;
   post_process_scan();
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 $rotating = TRUE;
 return;
}


# Analyze selected images

sub analyze_selected {
 my @page = $slist -> get_selected_indices; #should we just always analyze all?
 my @list;
 push @analyze_queue, [ $_ ] foreach (@page);

# Update undo/redo buffers
 take_snapshot();

 analyze();
 return;
}


# Analyze images in @analyze_queue 

sub analyze {
 my ($select_blank, $select_dark) = @_;
 my $page = 0;
 my $npages = 0;

# guess where unpaper has been called from
 my $window = get_parent($windows, $window);
 my $dialog = Gtk2::Dialog -> new ($d->get('Analyze')."...", $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;
  $analyzing = FALSE;
  kill_subs();
 });
 $dialog -> show_all;

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

 my ($child, $parent) = open_socketpair();
 my $pid = start_process(sub {
  send($parent, 'ready', 0);

  while (TRUE) {

# Now block until the GUI passes the filename
   my $rin = '';
   my $rout = '';
   my ($index, $filename);
   vec($rin, $parent->fileno(), 1) = 1;
   if (select($rout=$rin,undef,undef,undef)) {
    my $line;
    recv($parent, $line, 1000, 0);
    return if ($line eq 'finished');
    ($index, $filename) = split ' ', $line;
    print "index: $index $filename\n" if $debug;
   }

# Identify with imagemagick
   my $image = Image::Magick->new;
   my $x = $image->Read($filename);
   warn "$x" if "$x";

   my ($depth,$min,$max,$mean,$stddev) = $image->Statistics();
   warn "image->Statistics() failed" unless defined $depth;
   print "std dev: $stddev mean: $mean" if $debug;
   print "\n" if $debug;
   #my $quantum_depth = $image->QuantumDepth;
   #warn "image->QuantumDepth failed" unless defined $quantum_depth;
   #TODO add any other useful image analysis here e.g. is the page mis-oriented?
   #  detect mis-orientation possible algorithm:
   #   blur or low-pass filter the image (so words look like ovals)
   #   look at few vertical narrow slices of the image and get the Standard Deviation
   #   if most of the Std Dev are high, then it might be portrait  
   send($parent, "$index $filename $mean $stddev $depth", 0);  #TODO may need to send quantumdepth
  }
 });

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($child, $line, 1000, 0);
   my ($index, $filename, $mean, $stddev, $depth) = split ' ', $line;
   if (defined $filename and $filename ne '' and $index ne 'ready') {
    warn "index: $index depth: $depth\n" unless $depth;
    my $maxQ=-1+(1<<$depth);
    $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
    $slist -> {data}[$index][2] = $filename;
    $slist -> {data}[$index][1] = get_pixbuf($filename, $heightt, $widtht);
    $slist -> {data}[$index][6] = $maxQ?$mean/$maxQ:0;
    $stddev=0 if $stddev eq "nan";
    #normalize the Std Dev as in ImageMagick's "identify -verbose"
    $slist -> {data}[$index][7] = $maxQ?$stddev/$maxQ:0; 
    $slist -> {data}[$index][11] = timestamp();
    $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
    my @selected = $slist -> get_selected_indices;
    display_image($filename) if ($index == $selected[0]);
   }
   if (@analyze_queue) {
    $page++;
    $npages++;
    my ($index) = @{shift @analyze_queue};
    my $filename = $slist -> {data}[$index][2];
    send($child, "$index $filename", 0);
    $pbar->set_text(sprintf($d->get("Analyzing page %i of %i"), $page, $#analyze_queue+$npages+1));
    $pbar->set_fraction (($page-1)/($#analyze_queue+$npages+1));
   }
   else {
    send($child, 'finished', 0);
    $dialog -> destroy;
    $analyzing = FALSE;
    post_process_scan();
    select_blank_pages() if $select_blank;
    select_dark_pages() if $select_dark;
    return FALSE;  # uninstall
   }
  }

# 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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   $analyzing = FALSE;
   post_process_scan();
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 $analyzing = TRUE;
 return;
}


# 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')."...", $window,
                                            'modal',
                                            '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 ($child, $parent) = open_socketpair();
  my $pid = start_process(sub {

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

   foreach (@pagelist) {
    my $index=$_;
    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'}.'%');

# flag the page as modified so it can be re-OCRed and re-Analyzed
    $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
    $slist -> {data}[$index][8] = timestamp(); #flag as dirty
    $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});

# Write it
    my $suffix;
    $suffix = $1 if ($filename =~ /(\.\w*)$/);
    (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => $suffix);
    $x = $image->Write(filename => $filename);
    warn "$x" if "$x";
    warn "Thresholding at $SETTING{'threshold tool'} to $filename\n" if $debug;
    send($parent, sprintf("%i %i %s", $page++, $#pagelist+1, $filename), 0);
   }
   send($parent, '2 1', 0);
  });

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($child, $line, 1000, 0);
    my ($page, $total, $filename) = split ' ', $line;
    my $fraction = $page/$total;
    if ($fraction > 1) {
     $dialog -> destroy;
     return FALSE;  # uninstall
    }
    $pbar->set_fraction($fraction);
    $pbar->set_text(sprintf($d->get("Applying threshold to page %i of %i"),
                                                              $page+1, $total));
    if (defined $filename and $filename ne '') {
     $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
     $slist -> {data}[$pagelist[$page]][2] = $filename;
     $slist -> {data}[$pagelist[$page]][1] =
                                       get_pixbuf($filename, $heightt, $widtht);
     $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
    }
    display_image($filename) if (! $page);
   }

# 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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
    $dialog -> destroy;
    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;
 return;
}


# Display page selector and on apply negate accordingly

sub negate {

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

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

# Frame for page range
 add_page_range($vbox);

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

  my $dialog = Gtk2::Dialog -> new ($d->get('Applying negate')."...", $window,
                                            'modal',
                                            '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 ($child, $parent) = open_socketpair();
  my $pid = start_process(sub {

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

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

    my $depth = $image->Get('depth');

# Negate the image
    $image->Negate;

# Write it
    my $suffix;
    $suffix = $1 if ($filename =~ /(\.\w*)$/);
    (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => $suffix);
    $x = $image->Write(depth => $depth, filename => $filename);
    warn "$x" if "$x";
    warn "Negating to $filename\n" if $debug;
    send($parent, sprintf("%i %i %s", $page++, $#pagelist+1, $filename), 0);
   }
   send($parent, '2 1', 0);
  });

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($child, $line, 1000, 0);
    my ($page, $total, $filename) = split ' ', $line;
    my $fraction = $page/$total;
    if ($fraction > 1) {
     $dialog -> destroy;
     return FALSE;  # uninstall
    }
    $pbar->set_fraction($fraction);
    $pbar->set_text(sprintf($d->get("Applying negate to page %i of %i"),
                                                              $page+1, $total));
    if (defined $filename and $filename ne '') {
     $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
     $slist -> {data}[$pagelist[$page]][2] = $filename;
     $slist -> {data}[$pagelist[$page]][1] =
                                       get_pixbuf($filename, $heightt, $widtht);
     $slist -> {data}[$pagelist[$page]][8] = timestamp(); #flag as dirty
     $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
    }
    display_image($filename) if (! $page);
   }

# 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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
    $dialog -> destroy;
    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;
 return;
}


# Display page selector and on apply unsharp accordingly

sub unsharp {

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

 my ($windowum, $vbox) = create_window($window, $d->get('Unsharp mask'), TRUE);

# Frame for page range
 add_page_range($vbox);

# SpinButton for radius
 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start ($hbox, FALSE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Radius'));
 $hbox -> pack_start ($label, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('pixels'));
 $hbox -> pack_end ($label, FALSE, TRUE, 0);
 my $spinbuttonr = Gtk2::SpinButton -> new_with_range(0, 100, 1);
 $spinbuttonr->set_value ($SETTING{'unsharp radius'});
 $tooltips -> set_tip ($spinbuttonr, $d->get('The radius of the Gaussian, in pixels, not counting the center pixel (0 = automatic).'));
 $hbox -> pack_end ($spinbuttonr, FALSE, TRUE, 0);

# SpinButton for sigma
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start ($hbox, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Sigma'));
 $hbox -> pack_start ($label, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('pixels'));
 $hbox -> pack_end ($label, FALSE, TRUE, 0);
 my $spinbuttons = Gtk2::SpinButton -> new_with_range(0, 5, .1);
 $spinbuttons->set_value ($SETTING{'unsharp sigma'});
 $tooltips -> set_tip ($spinbuttons, $d->get('The standard deviation of the Gaussian.'));
 $hbox -> pack_end ($spinbuttons, FALSE, TRUE, 0);

# SpinButton for amount
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start ($hbox, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Amount'));
 $hbox -> pack_start ($label, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('%'));
 $hbox -> pack_end ($label, FALSE, TRUE, 0);
 my $spinbuttona = Gtk2::SpinButton -> new_with_range(0, 100, 1);
 $spinbuttona->set_value ($SETTING{'unsharp amount'});
 $tooltips -> set_tip ($spinbuttona, $d->get('The percentage of the difference between the original and the blur image that is added back into the original.'));
 $hbox -> pack_end ($spinbuttona, FALSE, TRUE, 0);

# SpinButton for threshold
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start ($hbox, FALSE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Threshold'));
 $hbox -> pack_start ($label, FALSE, TRUE, 0);
 my $spinbuttont = Gtk2::SpinButton -> new_with_range(0, 1, 0.01);
 $spinbuttont->set_value ($SETTING{'unsharp threshold'});
 $tooltips -> set_tip ($spinbuttont, $d->get('The threshold, as a fraction of QuantumRange, needed to apply the difference amount.'));
 $hbox -> pack_end ($spinbuttont, 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{'unsharp radius'} = $spinbuttonr->get_value;
  $SETTING{'unsharp sigma'} = $spinbuttons->get_value;
  $SETTING{'unsharp amount'} = $spinbuttona->get_value;
  $SETTING{'unsharp threshold'} = $spinbuttont->get_value;
  my $dialog = Gtk2::Dialog -> new ($d->get('Applying unsharp mask')."...", $window,
                                            'modal',
                                            '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 ($child, $parent) = open_socketpair();
  my $pid = start_process(sub {

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

   foreach (@pagelist) {
    send($parent, 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";

# Unsharp the image
    $image->UnsharpMask(radius=>$SETTING{'unsharp radius'},
                        sigma=>$SETTING{'unsharp sigma'},
                        amount=>$SETTING{'unsharp amount'},
                        threshold=>$SETTING{'unsharp threshold'});

# Write it
    my $suffix;
    $suffix = $1 if ($filename =~ /\.(\w*)$/);
    (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => '.'.$suffix);
    $x = $image->Write(filename => $filename);
    warn "$x" if "$x";
    warn "Written $filename with unsharp mask: r=$SETTING{'unsharp radius'}, s=$SETTING{'unsharp sigma'}, a=$SETTING{'unsharp amount'}, t=$SETTING{'unsharp threshold'}\n"
     if $debug;

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

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($child, $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;
      return FALSE;  # uninstall
     }
     $pbar->set_fraction($fraction);
     $pbar->set_text(sprintf($d->get("Applying unsharp mask to page %i of %i"),
                                                              $page+1, $total));
     if (defined $filename and $filename ne '') {
      $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
      $slist -> {data}[$pagelist[$page]][2] = $filename;
      $slist -> {data}[$pagelist[$page]][1] =
                                       get_pixbuf($filename, $heightt, $widtht);
      $slist -> {data}[$pagelist[$page]][8] = timestamp(); #flag as dirty
      $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
      display_image($filename) if (! $page);
     }
    }
   }

# 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 (not 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 { $windowum -> destroy } );

 $windowum -> show_all;
 return;
}


# Callback for change Gtk2::ImageView::Tool

sub change_image_tool_cb {
 my ($action, $current) = @_;
 my $value = $current->get_current_value ();
 my $tool = $selector;
 if ($value == 10) {
  $tool = $dragger;
 }
 elsif ($value == 30) {
  $tool = $painter;
 }
 $view->set_tool ($tool);
# if ($value == 20) {
#  sel_changed_cb ($selector, $sel_info_label);
# }
# else {
#  $sel_info_label->set_text ("");
# }
 return;
}


# Display page selector and on apply crop 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 {

  my $sel = $selector->get_selection;
  return if (! defined $sel);

# Update undo/redo buffers
  take_snapshot();

  my $dialog = Gtk2::Dialog -> new ($d->get('Cropping')."...", $window,
                                            'modal',
                                            '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 ($child, $parent) = open_socketpair();
  my $pid = start_process(sub {

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

   foreach (@pagelist) {
    send($parent, 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";

    my ($x, $y, $w, $h) = $sel->values;

# 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;
    $suffix = $1 if ($filename =~ /\.(\w*)$/);
    (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => '.'.$suffix);
    warn " to $filename\n" if $debug;
    $e = $image->Write(filename => $filename);
    warn "$e" if "$e";

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

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

   my $line;
   if ($condition & 'in') { # bit field operation. >= would also work
    recv($child, $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;
      return FALSE;  # uninstall
     }
     $pbar->set_fraction($fraction);
     $pbar->set_text(sprintf($d->get("Cropping page %i of %i"), $page+1, $total));
     if (defined $filename and $filename ne '') {
      $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
      $slist -> {data}[$pagelist[$page]][2] = $filename;
      $slist -> {data}[$pagelist[$page]][1] =
                                       get_pixbuf($filename, $heightt, $widtht);
      $slist -> {data}[$pagelist[$page]][8] = timestamp(); #flag as dirty
      $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
      display_image($filename) if (! $page);
     }
    }
   }

# 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 (not 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;
 return;
}


# Minimise gscan2pdf, feed GIMP with the current page, restore gscan2pdf when
# GIMP finishes

sub gimp {
 my @pages = $slist -> get_selected_indices;
 $window->iconify;
 Gtk2->main_iteration while Gtk2->events_pending;

# Update undo/redo buffers
 take_snapshot();

 $slist -> get_selection -> unselect_all;

 my $format;
 my $filename = $slist->{data}[$pages[0]][2];
 $format = $1 if ($filename =~ /\.(\w*)$/);

# PNMs don't store the resolution, so GIMP doesn't get it. Convert to TIFF
 if ($format eq 'pnm') {
  (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => '.tif');
  system("convert $slist->{data}[$pages[0]][2] -density $slist->{data}[$pages[0]][4] -units PixelsPerInch $filename");
 }
 else {
  (undef, $filename) = tempfile(DIR => $SETTING{session}, SUFFIX => ".$format");
  copy($slist->{data}[$pages[0]][2], $filename) or 
   show_message_dialog($window, 'error', 'close', $d->get('Error copying page'));
 }
 $slist->{data}[$pages[0]][2] = $filename;
 system("gimp $filename");

# Update thumbnail
 $slist->{data}[$pages[0]][1] = get_pixbuf($filename, $heightt, $widtht);
 $window->deiconify;
 Gtk2->main_iteration while Gtk2->events_pending;
 $slist -> select(@pages);
 return;
}


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


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

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

# guess where unpaper has been called from
 my $parent = get_parent($windowu, $windows, $window);
 my $dialog = Gtk2::Dialog -> new ($d->get('Running unpaper')."...", $parent,
                                   'modal',
                                   '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
 $running_unpaper = 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 
 Glib::Timeout->add (100, sub {

# To prevent any further pages being processed
  if ($cancelled) {
   $running_unpaper = FALSE;
   return FALSE;  # uninstall
  }
  elsif (@unpaper_stack) {
   if (! $running_unpaper) {
    $running_unpaper = TRUE;
    $page++;
    $npages++;
    my ($pagenum, $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 => $SETTING{session}, SUFFIX => '.pnm');
    my $out2 = '';
    (undef, $out2) = tempfile(DIR => $SETTING{session}, SUFFIX => '.pnm')
     if ($options =~ /--output-pages 2 /);

    if ($slist -> {data}[$pagenum][2] !~ /\.pnm$/) {
     my $image = Image::Magick->new;
     my $x = $image->Read($slist -> {data}[$pagenum][2]);
     warn "$x" if "$x";
     my $depth = $image->Get('depth');

# Unforunately, -depth doesn't seem to work here, so forcing depth=1 using pbm extension.
     my $suffix = ".pbm";
     $suffix = ".pnm" if ($depth > 1);
     (undef, $in) = tempfile(DIR => $SETTING{session}, SUFFIX => $suffix);
     $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 --input-file-sequence $in --output-file-sequence $out $out2;";
    $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 (not 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 -> {row_changed_signal});
      $slist -> {data}[$pagenum][2] = $out;
      $slist -> {data}[$pagenum][1] = get_pixbuf($out, $heightt, $widtht);
 
      if ($options =~ /--output-pages 2 /) {
       my $index = import_scan($out2, $slist -> {data}[$pagenum][0],
                      'Portable anymap', $slist -> {data}[$pagenum][4], TRUE)+1;

# Update stacks as we have inserted a page
       for (my $i = 0; $i < @unpaper_stack; ++$i) {
        ++$unpaper_stack[$i][0] if ($unpaper_stack[$i][0] > $pagenum);
       }
# Using $ocr rather than pushing @ocr_stack inside for() to avoid infinite loop
       my $ocr = FALSE;
       for (@ocr_stack) {
        if ($_ > $pagenum) {
         ++$_;
        }
        elsif ($_ == $pagenum) {
         $ocr = TRUE;
        }
       }
       push @ocr_stack, $index if $ocr;

# Using $selected rather than pushing @selection inside for() to avoid infinite loop
       my $selected = FALSE;
       for (@selection) {
        if ($_ > $pagenum) {
         ++$_;
        }
        elsif ($_ == $pagenum) {
         $selected = TRUE;
        }
       }
       push @selection, $index if $selected;

# Sort out numbering as now we have duplicates
       renumber($slist, 0);
      }
      $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
      
# Reselect selected pages, firing the display callback
      $slist -> get_selection -> unselect_all;
      $slist -> select(@selection);

      $running_unpaper = FALSE;

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

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


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

 my $widget;
 $SETTING{$option} = $hashref->{$option}{default}
  if (defined($hashref->{$option}{default}) and not 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;
  @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};
   }
  }
 }
 return;
}


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');
 my $outpages = add_widget($vbox, $unpaper_options, 'output-pages');
 $combobl -> signal_connect (changed => sub {
  if ($combobl -> get_active == 0) {
   $outpages->set_range(1, 2);
  }
  else {
   $outpages->set_range(1, 1);
  }
 });

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


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


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

sub ocr_page {
 my @pages = @_;
 push @ocr_stack, @pages;

# guess where ocr has been called from
 my $parent = get_parent($windowo, $windows, $window);
 my $dialog = Gtk2::Dialog -> new ($d->get('Running OCR')."...", $parent,
                                   'modal',
                                   '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
 $running_ocr = 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 
 Glib::Timeout->add (100, sub {

# To prevent any further pages being processed
  if ($cancelled) {
   $running_ocr = FALSE;
   return FALSE;  # uninstall
  }
  elsif (@ocr_stack) {
   if (! $running_ocr) {
    $running_ocr = 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 => $SETTING{session});

    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 => $SETTING{session}, 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 => $SETTING{session}, 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 (not 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_ocr = FALSE;

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

# Slurp the OCR output
      if ($SETTING{'OCR output'} eq 'replace') {
       $slist -> {data}[$pagenum][3] = slurp("$txt.txt"); 
      }
      elsif ($SETTING{'OCR output'} eq 'append') {
       $slist -> {data}[$pagenum][3] .= slurp("$txt.txt"); 
      }
      else {
       $slist -> {data}[$pagenum][3] = slurp("$txt.txt") . $slist -> {data}[$pagenum][3]; 
      }
      unlink <$txt*>;
      $slist -> {data}[$pagenum][9] = 1; #FlagOCR
      $slist -> {data}[$pagenum][10] = timestamp(); #remember when we ran OCR on this page
      $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});

# 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_ocr) {
   $dialog -> destroy;
   return FALSE;  # uninstall
  }
  else {
   return TRUE;  # continue without uninstalling
  }
 });
 return;
}


# Have to roll my own slurp sub to support utf8

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

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


# Create a combobox from an array and set the default

sub combobox_from_array {
 my (@array) = @_;

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


# Create a combobox from an array and set the default

sub combobox_set_active {
 my ($combobox, $default, @array) = @_;
 
# Fill ComboBox
 my $i = 0;
 my $o;
 if (defined($default)) {
  foreach ( @array ) {
   $o = $i if (defined($_->[0]) and $_->[0] eq $default);
   ++$i;
  }
 }
 $o = 0 if (! defined $o);
 $combobox -> set_active ($o);
 return;
}


# 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;
   $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(@tesslang);
 combobox_set_active($combobox, $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(@ocr_engine);
 combobox_set_active($combobe, $SETTING{'ocr engine'}, @ocr_engine);
 $hboxe -> pack_end ($combobe, FALSE, FALSE, 0);
 my ($comboboxl, @tesslang, $hboxl);
 if ($dependencies{tesseract}) {
  ($hboxl, $comboboxl, @tesslang) = add_ocr_languages($vbox);
  $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;
 $hboxl->hide_all
  if ($dependencies{tesseract}
                   and $ocr_engine[$combobe -> get_active]->[0] ne 'tesseract');
 return;
}


# 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 <$SETTING{session}/*>;
 rmdir $SETTING{session};
 delete $SETTING{session};

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

# delete $SETTING{'RMB'};

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

# kill all threads
 for (1..$numthreads) {
  $shash{$_}{die} = 1;
  $shash{$_}{semaphore}->up;
  $thread{$_}->join;
 }

 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"), $prog_name, "--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;
 return;
}


# Remove formatting characters

sub strippod {
 my $text = shift;
 $text =~ s/B<([^<]*)>/$1/g;
 $text =~ s/E<gt>/>/g;
 return $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;
 }
 return;
}


# Get option string from label

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


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


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

# Save session
 save_session();
 return;
}


# Dump $slist to a text file.
# If a filename is given, zip it up as a session file

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

 my %session;
 for (my $i = 0; $i <= $#{$slist->{data}}; $i++) {
  $session{$slist->{data}[$i][0]}{filename}   = $slist->{data}[$i][2];
  $session{$slist->{data}[$i][0]}{buffer}     = $slist->{data}[$i][3]
   if (defined $slist->{data}[$i][3]);
  $session{$slist->{data}[$i][0]}{resolution} = $slist->{data}[$i][4];
 }
 $session{selection} = $slist -> get_selected_indices;
 my $sessionfile = "$SETTING{session}/session";
 system("touch $sessionfile");
 my $session = Config::General->new(-ConfigFile => $sessionfile);
 $session->save_file($sessionfile, \%session);
 if (defined $filename) {
  my $tar = Archive::Tar->new;
  my @filenamelist = glob("*");
  print Dumper(\@filenamelist) if $debug;
  $tar->add_files(@filenamelist);
  $tar->write($filename, TRUE, '');
 }
 return;
}


# 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 -> {row_changed_signal});
 @{$slist->{data}} = @undo_buffer;

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

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


# 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 -> {row_changed_signal});
 @{$slist->{data}} = @redo_buffer;

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

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


# Initialise IconFactory

sub init_icons {
 my @icons = @_;
 return if defined $IconFactory;

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

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


# 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 `$path': $@");
 }
 else {
  my $set = Gtk2::IconSet->new_from_pixbuf($icon);
  $IconFactory->add($stock_id, $set);
 }
 return;
}


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


sub open_socketpair {
 my $child = FileHandle->new;
 my $parent = FileHandle->new;
 socketpair($child, $parent,  AF_UNIX, SOCK_DGRAM, PF_UNSPEC);
 binmode $child, ':utf8';
 binmode $parent, ':utf8';
 return ($child, $parent);
}


# Fork the passed sub

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

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

# We're still in the parent; note pid and watch the streams:
  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.

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


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


# marked page list as saved

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


# 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,
                                    'modal',
                                    '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 ($child, $parent) = open_socketpair();
 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($parent, $i/@filename." $i $filename[$i] "
                                             .$d->get('Compressing images'), 0);
  }

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

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

  my $line;
  if ($condition & 'in') { # bit field operation. >= would also work
   recv($child, $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 -> {row_changed_signal});
      unlink $slist -> {data}[$page][2];
      $slist -> {data}[$page][2] = $filename;
      $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
     }
    }
   }
  }

# 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 (not defined($line) or $line eq '')) { # bit field operation. >= would also work
   $dialog -> destroy;
   return FALSE;  # uninstall
  }
  return TRUE;  # continue without uninstalling
 });
 return;
}


# Preferences dialog

sub preferences {

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

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

# Frontends
 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Frontend'));
 $hbox -> pack_start($label, FALSE, FALSE, 0);
 my @frontends = (
  [ 'libsane-perl', $d->get('libsane-perl'), $d->get('Scan using the perl bindings for SANE.') ],
  [ 'scanimage', $d->get('scanimage'), $d->get('Scan using the scanimage frontend.') ],
  [ 'scanimage-perl', $d->get('scanimage-perl'), $d->get('Scan using the scanimage-perl frontend.') ],
  [ 'scanadf-perl', $d->get('scanadf-perl'), $d->get('Scan using the scanadf-perl frontend.') ],
 );
 push @frontends,
  [ 'scanadf', $d->get('scanadf'), $d->get('Scan using the scanadf frontend.') ],
  if ($dependencies{scanadf});
 my $combob =  combobox_from_array(@frontends);
 combobox_set_active($combob, $SETTING{frontend}, @frontends);
 $hbox -> pack_end($combob, TRUE, TRUE, 0);

# scan command prefix
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Scan command prefix'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);
 my $preentry = Gtk2::Entry -> new;
 $hbox -> add($preentry);
 $preentry -> set_text($SETTING{'scan prefix'}) if defined($SETTING{'scan prefix'});

# Restore window setting
 my $cbw = Gtk2::CheckButton->new_with_label(
                                 $d->get('Restore window settings on startup'));
 $cbw->set_active (TRUE) if ($SETTING{'restore window'});
 $vbox -> pack_start($cbw, TRUE, TRUE, 0);

# Cache options?
 my $cbc = Gtk2::CheckButton -> new_with_label($d->get('Cache device-dependent options'));
 $cbc->set_active (TRUE) if ($SETTING{'cache options'});
 $vbox -> pack_start($cbc, TRUE, TRUE, 0);

# Clear options cache
 my $button = Gtk2::Button -> new($d->get('Clear device-dependent options cache'));
 $vbox -> pack_start($button, TRUE, TRUE, 0);
 $button -> signal_connect( clicked => sub {
  delete $SETTING{cache} if (defined $SETTING{cache});
 });

# Temporary directory settings
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Temporary directory'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);
 my $tmpentry = Gtk2::Entry -> new;
 $hbox -> add($tmpentry);
 $tmpentry -> set_text(dirname($SETTING{session}));
 $button = Gtk2::Button -> new($d->get('Browse'));
 $button -> signal_connect( clicked => sub {
  my $file_chooser = Gtk2::FileChooserDialog -> new(
                                       $d->get('Select temporary directory'),
                                       $windowr, 'select-folder',
                                       'gtk-cancel' => 'cancel',
                                       'gtk-ok' => 'ok');
  $file_chooser->set_current_folder ($tmpentry -> get_text);
  if ('ok' eq $file_chooser->run) {
   $tmpentry -> set_text($file_chooser -> get_filename);
  }
  $file_chooser -> destroy;
 });
 $hbox -> pack_end( $button, TRUE, TRUE, 0 );

# Blank page standard deviation threshold
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Blank threshold'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);
 my $spinbuttonb = Gtk2::SpinButton -> new_with_range(0, 1, 0.001);
 $spinbuttonb->set_value($SETTING{'Blank threshold'});
 $tooltips -> set_tip ($spinbuttonb, $d->get('Threshold used for selecting blank pages'));
 $hbox -> add($spinbuttonb);

# Dark page mean threshold
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('Dark threshold'));
 $hbox -> pack_start ($label, FALSE, FALSE, 0);
 my $spinbuttond = Gtk2::SpinButton -> new_with_range(0, 1, 0.01);
 $spinbuttond->set_value($SETTING{'Dark threshold'});
 $tooltips -> set_tip ($spinbuttond, $d->get('Threshold used for selecting dark pages'));
 $hbox -> add($spinbuttond);

# OCR output
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $label = Gtk2::Label -> new ($d->get('OCR output'));
 $hbox -> pack_start($label, FALSE, FALSE, 0);
 my @array = (
  [ 'replace', $d->get('Replace'), $d->get('Replace the contents of the text buffer with that from the OCR output.') ],
  [ 'prepend', $d->get('Prepend'), $d->get('Prepend the OCR output to the text buffer.') ],
  [ 'append', $d->get('Append'), $d->get('Append the OCR output to the text buffer.') ],
 );
 my $comboo =  combobox_from_array(@array);
 combobox_set_active($comboo, $SETTING{'OCR output'}, @array);
 $hbox -> pack_end($comboo, TRUE, TRUE, 0);

# Apply button
 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 $button = Gtk2::Button -> new_from_stock('gtk-apply');
 $hbox -> pack_start( $button, TRUE, TRUE, 0 );
 $button -> signal_connect (clicked => sub {
  $windowr -> hide;
  if ($SETTING{frontend} ne $frontends[$combob->get_active][0]) {
   $SETTING{frontend} = $frontends[$combob->get_active][0];
   if ($SETTING{frontend} eq 'libsane-perl') {
    $windows->destroy if (defined $windows);
   }
   else {
    $windows2->destroy if (defined $windows2);
    rescan_options($vboxd, $device[$combobd -> get_active]) if (defined $windows);
   }
  }
  $SETTING{'scan prefix'} = $preentry -> get_text;
  $SETTING{'cache options'} = $cbc->get_active;
  delete $SETTING{cache}
   if (defined $SETTING{cache} and not $SETTING{'cache options'});
  $SETTING{'restore window'} = $cbw->get_active;

  my @tmpdirs = File::Spec->splitdir($SETTING{session});
  pop @tmpdirs; # Remove the top level
  my $tmp = File::Spec->catdir(@tmpdirs);

# Expand tildes in the filename
  my $newdir = $tmpentry -> get_text;
  $newdir =~ s{ ^ ~ ( [^/]* ) } {
   $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($>))[7] )
  }ex;

  if ($newdir ne $tmp) {
   $SETTING{TMPDIR} = $newdir;
   show_message_dialog($window, 'warning', 'close', $d->get('You will have to restart gscanp2df for changes to the temporary directory to take effect.'));
  }
  $SETTING{'Blank threshold'} = $spinbuttonb->get_value;
  $SETTING{'Dark threshold'} = $spinbuttond->get_value;
  $SETTING{'OCR output'} = $array[$comboo->get_active][0];
 });

# Cancel button
 $button = Gtk2::Button -> new_from_stock('gtk-cancel');
 $hbox -> pack_end( $button, TRUE, TRUE, 0 );
 $button -> signal_connect (clicked => sub {
  $windowr -> hide;
 });
 $windowr -> show_all;
 return;
}


sub properties {

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

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

 my $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);
 my $label = Gtk2::Label -> new ($d->get('Resolution'));
 $hbox -> pack_start($label, FALSE, FALSE, 0);
 my $entry = Gtk2::Entry->new;
 $entry->set_width_chars (4);
 $entry->set_activates_default (TRUE);
 $entry->signal_connect ('insert-text' => sub {
  my ($widget, $string, $len, $position) = @_;
  # just can't insert these.
  for (split '', $string) {
   if ($_ lt 0 or $_ gt 9) {
    $entry->signal_stop_emission_by_name('insert-text');
    last;
   }
  }
  () # this callback must return either 2 or 0 items.
 });

 $entry->set_text(get_selected_properties());
 $slist -> get_selection -> signal_connect(changed => sub {
  $entry->set_text(get_selected_properties());
 });
 $hbox -> pack_end($entry, TRUE, TRUE, 0);

 $hbox = Gtk2::HBox -> new;
 $vbox -> pack_start($hbox, TRUE, TRUE, 0);

# Apply button
 my $button = Gtk2::Button -> new_from_stock('gtk-apply');
 $button->can_default (TRUE);
 $windowp->set_default ($button);
 $hbox -> pack_start( $button, TRUE, TRUE, 0 );
 $button -> signal_connect (clicked => sub {
  $windowp -> hide;
  my $resolution = $entry->get_text;
  $slist -> get_model -> signal_handler_block($slist -> {row_changed_signal});
  for ($slist -> get_selected_indices) {
   $slist -> {data}[$_][4] = $resolution;
  }
  $slist -> get_model -> signal_handler_unblock($slist -> {row_changed_signal});
 });

# Cancel button
 $button = Gtk2::Button -> new_from_stock('gtk-cancel');
 $hbox -> pack_end( $button, TRUE, TRUE, 0 );
 $button -> signal_connect (clicked => sub {
  $windowp -> hide;
 });
 $windowp -> show_all;
 return;
}


# Helper function for properties()
sub get_selected_properties {
 my @page = $slist -> get_selected_indices;
 my $resolution = '';
 $resolution = $slist -> {data}[shift @page][4] if (@page > 0);
 for (@page) {
  if ($slist -> {data}[$_][4] != $resolution) {
   $resolution = '';
   last;
  }
 }
 return $resolution;
}


# Return file size expected by PNM header
sub get_size_from_PNM {
 my $filename = shift;

 open my $fh, '<', $filename or return 0;
 my $header = <$fh>;
 my $magic_value;
 if ($header =~ /^P(\d*)\n/) {
  $magic_value = $1;
 }
 else {
  close $fh;
  return 0;
 }
 if ($magic_value < 4) {
  close $fh;
  return 0;
 }
 my $line = <$fh>;
 $header .= $line;
 while ($line =~ /^(#|\s*\n)/) {
  $line = <$fh>;
  $header .= $line;
 }
 if ($line =~ /(\d*) (\d*)\n/) {
  my ($width, $height) = ($1, $2);
  my $hundred_percent = $width * $height
                      * ($magic_value == 4 ? 1/8 : ($magic_value == 5 ? 1 : 3));
  if ($magic_value > 4) {
   $line = <$fh>;
   $header .= $line;
  }
  close $fh;
  return length($header)+$hundred_percent;
 }
 else {
  close $fh;
  return 0;
 }
}


Gtk2 -> main;


package Gscan2pdf::PageRange; ## no critic

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(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 selected or all', #blurb
               'Gscan2pdf::PageRange::Range',
               'selected', # default
               [qw/readable writable/] #flags
        ),
    ]
    ;


sub INIT_INSTANCE {
 my $self = shift;
 my %buttons = (
  '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});
 }
 return;
}


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


__END__

=head1 Name

gscan2pdf - A GUI to produce a multipage PDF or DjVu from a scan.

=for html <p align="center">
 <img src="http://sourceforge.net/dbimage.php?id=171868" border="1" width="632"
 height="480" alt="Screenshot" /><br/>Screenshot: Main page v0.9.24</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

=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 Debian, you should find that sid has the latest version already
packaged.

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

C<deb http://ppa.launchpad.net/jeffreyratcliffe/ubuntu E<lt>releaseE<gt> main>

C<deb-src http://ppa.launchpad.net/jeffreyratcliffe/ubuntu E<lt>releaseE<gt> main>

where C<E<lt>releaseE<gt>> is the version of Ubuntu you are using.

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. Fetch the key:

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

Then add it to the apt keyring:

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

=head2 RPMs

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

=head2 From source

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

=head2 From the repository

gscan2pdf uses Git for its Revision Control System. You can browse the
tree at http://gscan2pdf.git.sourceforge.net/git/gitweb.cgi?p=gscan2pdf

Git users can clone the complete tree with
C<git clone git://gscan2pdf.git.sourceforge.net/gitroot/gscan2pdf>

=head1 Building gscan2pdf from source

Having downloaded the source either from a Sourceforge file release, or from the
Git 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-perl (>= 1:1.043-1)

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

=item libgtk2-imageview-perl

Perl bindings to the gtkimageview widget.
See L<http://trac.bjourne.webfactional.com/>

=item libgtk2-ex-simple-list-perl

A simple interface to Gtk2's complex MVC list widget

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

Perl bindings for libsane.

=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, preferably against the Debian package[1][2].
You do not need to be a Debian user, or set up an account to do this.

=over

=item 1. http://packages.debian.org/sid/gscan2pdf

=item 2. http://www.debian.org/Bugs/

=back

Alternatively, there is a bug tracker for 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://translations.launchpad.net/gscan2pdf>

Note that the translations for the scanner options are taken
directly from sane-backends. If you would like to contribute to these, you can
do so either at contact the sane-devel mailing list
(sane-devel@lists.alioth.debian.org) and have a look at the po/ directory in
the source code L<http://www.sane-project.org/cvs.html>.

Alternatively, Ubuntu has its own translation project. For the 9.04 release, the
translations are available at
L<https://translations.launchpad.net/ubuntu/jaunty/+source/sane-backends/+pots/sane-backends>

=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

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

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

=head4 DjVu

Both black and white, and colour images produce better
compression than PDF. See L<http://www.djvuzone.org/> for more details.

=head3 Email as PDF

Attaches the 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.

=head3 Compress temporary files

If your temporary ($TMPDIR) directory is getting full, this function can be useful -
compressing all images at LZW-compressed TIFFs. These require much less space than
the PNM files that are typically produced by SANE or by importing a PDF.

=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 than
scanimage with some scanners. On Debian-based systems, scanadf is in the sane package,
not, like scanimage, in sane-utils. If scanadf is not present, the option is
obviously ghosted out.

In 0.9.27, Perl bindings for SANE were introduced and two further frontends,
scanimage-perl and scanadf-perl (scanimage and scanadf transliterated from C into
Perl) was added.

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

The unsharp option sharpens an image. The image is convolved with a Gaussian
operator of the given radius and standard deviation (sigma). For reasonable
results, radius should be larger than sigma. Use a radius of 0 to have the
method select a suitable radius.

=head3 Crop

=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), unpaper and the rotate options
require imagemagick.

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

=head2 Why can gscan2pdf not find my scanner?

If your scanner is not connected directly to the machine on which you are
running gscan2pdf and you have not installed the SANE daemon, saned,
gscan2pdf cannot automatically find it. In this case, you can specify the
scanner device on the command line:

C<gscan2pdf --device <device>>

=head1 To Do

=over

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

=item *
Print (using Net::Cups).

=item *
Copier.

=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 *
The SANE project for scanner access

=item *
Björn Lindqvist for the gtkimageview widget

=item *
Sourceforge for hosting the project.

=back

=for html <hr />
 <a href="http://sourceforge.net/projects/gscan2pdf">
 <img src="http://sflogo.sourceforge.net/sflogo.php?group_id=174140&amp;type=14" width="150" height="40" alt="Get gscan2pdf at SourceForge.net. Fast, secure and Free Open Source software downloads" /></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
