#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process
# Copyright (C) 2006--2011 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
#*** unhandled exception in callback:
#***   Can't call method "hide_all" on an undefined value at bin/gscan2pdf line 5512.
#***  ignoring at bin/gscan2pdf line 12380.
#    add option for number of background processes
#     does Thread::Queue play nicely with forks?
#     grep -c '^processor' /proc/cpuinfo
#     use Linux::Cpuinfo;
#     my $cpuinfo = Linux::Cpuinfo->new();
#     $cnt  = $cpuinfo->num_cpus();
#    Saving large numbers of PNGs, etc. blocks
#    Support multiple sessions
#    Test coverage http://search.cpan.org/~pjcj/Devel-Cover-0.65/
#    It looks as though saving as PDF or DjVu with corrupted OCR output crashes gscan2pdf
#    Add Edit/Select/No OCR
#    Ask Fred http://www.fmwconcepts.com/imagemagick/ about techniques to split image into text/background
#      1. threshold image into threshold.png
#      2. convert white to alpha for foreground: convert threshold.png -negate -alpha copy -negate foreground.png
#      3. subtract from original image to produce background: composite -compose minus original.png foreground.png background.png
#     How do you fill the alpha layer in the background with white?
#     Use in djvu with:
#      1. convert foreground to DjVu Bitonal RLE format with
#          convert foreground.png foreground.pbm
#          pbmtodjvurle foreground.pbm > foreground.rle
#      2. convert background to ppm
#          convert background.png background.ppm
#      3. create a file for the text layer with #T commands
#      3. concatenate the three:
#          cat foreground.rle background.ppm text > sep
#      4. create the djvu:
#          csepdjvu sep file.djvu
#     For PDF put the background on top of the text, and then the foreground on top of that.
#    move po to share/locale
#    look at shutter, and see how he uses goocanvas together with gtkimageview
#    get canvas scrolling with imageview
#    add find text in ocr buffer - search through all buffers
#    window too tall after options have been found
#    add profiles
#    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
#    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 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?)
#    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
#    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.
#
# Release procedure:
#    Use
#      perltidy -i=1
#      TEST_AUTHOR=1 make test
#    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 (https://launchpad.net/gscan2pdf/+topcontributors)
#    Check a locale with LANG=de_DE.utf8 LANGUAGE=de_DE:de LC_CTYPE="de_DE.utf8" LC_NUMERIC="de_DE.utf8" LC_TIME="de_DE.utf8" LC_COLLATE="de_DE.utf8" LC_MONETARY="de_DE.utf8" LC_MESSAGES="de_DE.utf8" LC_PAPER="de_DE.utf8" LC_NAME="de_DE.utf8" LC_ADDRESS="de_DE.utf8" LC_TELEPHONE="de_DE.utf8" LC_MEASUREMENT="de_DE.utf8" LC_IDENTIFICATION="de_DE.utf8" bin/gscan2pdf --log=log
#    git ls-files > MANIFEST
#    Check $version
#    Make appropriate updates to debian/changelog
# 2. perl Makefile.PL
#    make rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf-x.x.x_all.deb
# 3. git status
#    git tag vx.x.x
#    git push --tags ssh://ra28145@gscan2pdf.git.sourceforge.net/gitroot/gscan2pdf/gscan2pdf master
# 4. make remote-html
# 5. create version directory in https://sourceforge.net/project/admin/explorer.php?group_id=174140
#    make file_releases
# 6. Freshmeat (requires summary of changes)
# 7. Launchpad (https://launchpad.net/gscan2pdf/trunk/+addrelease), 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@lists.sourceforge.net, gnome-announce-list@gnome.org, sane-devel@lists.alioth.debian.org
#    ftp-master (check contents with dpkg-deb --contents)

use warnings;
use strict;
use Gscan2pdf;
use Gtk2 -init;
use Gtk2::ImageView;
use Goo::Canvas;
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(store retrieve);
use Proc::Killfam;
use Proc::ProcessTable;
use Log::Log4perl;

# 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

use Readonly;
Readonly my $POINTS_PER_INCH => 72;

my $prog_name = 'gscan2pdf';
my $version   = '0.9.32';

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,   $log,         $log_level, @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,
 'log=s'        => \$log,
 'debug'        => sub { $log_level = 'DEBUG'; $debug = TRUE },
 'info'  => sub { $log_level = 'INFO' },
 'warn'  => sub { $log_level = 'WARN' },
 'error' => sub { $log_level = 'ERROR' },
 'fatal' => sub { $log_level = 'FATAL' },
 'version' => sub { warn "$prog_name $version\n"; exit 0 },
);
exit 1 if ( !GetOptions(@args) );
$Sane::DEBUG = $debug;

if ( not defined($log_level) ) {
 if ( defined $log ) {
  $log_level = 'DEBUG';
  $debug     = TRUE;
 }
 else {
  $log_level = 'ERROR';
 }
}
my $log_conf = qq/ 
 log4perl.appender.Screen        = Log::Log4perl::Appender::Screen 
 log4perl.appender.Screen.layout = Log::Log4perl::Layout::SimpleLayout
/;

if ( defined $log ) {
 $log_conf .= qq/
 log4perl.appender.Logfile          = Log::Log4perl::Appender::File 
 log4perl.appender.Logfile.filename = $log
 log4perl.appender.Logfile.mode     = write 
 log4perl.appender.Logfile.layout   = Log::Log4perl::Layout::SimpleLayout
 log4perl.category                  = $log_level, Logfile, Screen 
/;
}
else {
 $log_conf .= qq/
 log4perl.category                  = $log_level, Screen 
/;
}

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

Log::Log4perl::init( \$log_conf );
my $logger = Log::Log4perl::get_logger();

$logger->info("Starting $prog_name $version");
$logger->info("Log level $log_level");

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" );
$logger->info( "Using ", setlocale(LC_CTYPE), " locale" );
$logger->info( "Startup LC_NUMERIC ", setlocale(LC_NUMERIC) );

# Read config file
my $rc = "$ENV{'HOME'}/.$prog_name";
$logger->info("Reading config from $ENV{'HOME'}/.$prog_name");
system("touch $rc") if ( !-r $rc );
my ( $conf, %SETTING );
if (
 eval {
  $conf = Config::General->new(
   -ConfigFile  => $rc,
   -SplitPolicy => 'equalsign',
   -UTF8        => 1,
  );
 }
  )
{
 %SETTING = $conf->getall;
}
else {
 $logger->error(
  $d->get(
   "Error: unable to load settings.\nBacking up settings\nReverting to defaults"
  )
 );
 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,
 'default filename'  => '%a %y-%m-%d',
 'scan prefix'       => '',
 'Blank threshold'   => 0.005,         # Blank page standard deviation threshold
 'Dark threshold'    => 0.12,          # Dark page mean threshold
 'ocr engine'        => 'ocropus',
 '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,
  },
 },
 user_defined_tools => ['gimp %i']
);
delete $SETTING{frontend}
  if (
 defined $SETTING{frontend}
 and ( $SETTING{frontend} eq 'scanimage-perl'
  or $SETTING{frontend} eq 'scanadf-perl'
  or $SETTING{frontend} eq 'scanimage.pl'
  or $SETTING{frontend} eq
  'scanadf.pl' )    # these frontends are just for testing purposes
  );
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;
  $logger->debug("'$file','$device'");
  $file = getcwd . "/$file" if ( $file !~ /^\// );
  if ( !-r $file ) {
   $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $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 ) {
 $test_image = expand_tildes($test_image);
 if ( -r $test_image ) {
  $logger->info("Using test image $test_image");
 }
 else {
  $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $test_image ) );
  exit 1;
 }
}

$logger->info("Gtk2-Perl version $Gtk2::VERSION");
$logger->info( "Built for GTK " . join( '.', Gtk2->GET_VERSION_INFO ) );
$logger->info( "Running with GTK " . join( '.', Gtk2->get_version_info ) );
$logger->info( "Using GtkImageView version ",
 Gtk2::ImageView->library_version );
$logger->info("Using Gtk2::ImageView version $Gtk2::ImageView::VERSION");
$logger->info("Using PDF::API2 version $PDF::API2::Version::CVersion{vShort}");
if ($debug) {
 use Data::Dumper;
 $logger->debug( 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 SANE backend
Gscan2pdf::Backend::Sane->setup();

# 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, $print_settings,

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

 # Goo::Canvas for OCR output
 $canvas,

 # Notebook on scan dialog
 $notebook,

 # Scan button on scan dialog
 $sbutton,

 # List of PageRange widgets
 @prlist,

 # List of user-defined tools
 @user_defined_tools,
);

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'),
  '<control>g',
  $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
 ],
 [
  'Print',          'gtk-print', $d->get('_Print'), '<control>p',
  $d->get('Print'), \&print_dialog
 ],
 [
  '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('_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
 ],
 [
  '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
 ],
 [ 'User-defined', undef, $d->get('U_ser-defined') ],

 # 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'/>
   <menuitem action='Print'/>
   <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'/>
   <separator/>
   <menuitem action='unpaper'/>
   <menuitem action='OCR'/>
   <separator/>
   <menu action='User-defined'>
   </menu>
  </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'/>
  <toolitem action='Print'/>
  <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'/>
  <menuitem action='Print'/>
  <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 {
  if ( quit() ) {
   Gtk2->main_quit;
  }
  else {
   return TRUE;
  }
 }
);

# 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"); };
$logger->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 misc other info
Gtk2::Ex::Simple::List->add_column_type(
 'hstring',
 type => 'Glib::Scalar',
 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);

# Notebook for detail view and OCR output
my $vnotebook = Gtk2::Notebook->new;
$hpaned->pack2( $vnotebook, 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);
$vnotebook->append_page( $view, $d->get('Image') );

$view->signal_connect(
 button_press_event => sub {
  my ( $widget, $event ) = @_;
  handle_clicks( $widget, $event );
 }
);
$view->signal_connect( button_release_event => \&handle_clicks );
$view->signal_connect(
 'zoom-changed' => sub {
  $canvas->set_scale( $view->get_zoom ) if ( defined $canvas );
 }
);

# ScrolledWindow for Goo::Canvas for OCR output
my $scwin_buffer = Gtk2::ScrolledWindow->new;
$scwin_buffer->set_policy( 'automatic', 'automatic' );
$scwin_buffer->set_shadow_type('etched-in');
$vnotebook->append_page( $scwin_buffer, $d->get('OCR Output') );

# 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
  $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
  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
   $canvas = $slist->{data}[ $page[0] ][3];

# $canvas variable needed to prevent ->add() munging $slist -> {data}[$page[0]][3]
   if ( defined $canvas ) {
    $scwin_buffer->add($canvas);
    $canvas->show;
   }
  }
  else {
   $view->set_pixbuf(undef);
  }

  # 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   => 1000,
    step  => 1,
    order => 0,
   },
   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   => 1000,
    step  => 1,
    order => 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 ( defined $SETTING{pid} ) {
  my $t = Proc::ProcessTable->new;
  for my $p ( @{ $t->table } ) {
   if ( $p->pid == $SETTING{pid} and $p->cmndline =~ /gscan2pdf/ ) {
    show_message_dialog( $window, 'error', 'close',
     'Cannot run multiple instances of gscan2pdf' );
    kill_threads();
    die "Error: Cannot run multiple instances of gscan2pdf\n";
   }
  }
 }

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

# Note PID to able to tell if session has crashed or not
$SETTING{pid} = $$;

# 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;
   $logger->warn(
    sprintf(
     $d->get(
"Warning: unable to use %s for temporary storage. Defaulting to %s instead."
     ),
     $SETTING{TMPDIR},
     dirname( $SETTING{session} )
    )
   );
  }
 }
 else {
  $SETTING{session} = tempdir;
 }
 $logger->info("Using $SETTING{session} for temporary files");

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

 $SETTING{user_defined_tools} = [ $SETTING{user_defined_tools} ]
   if ( ref( $SETTING{user_defined_tools} ) ne 'ARRAY' );
 for ( @{ $SETTING{user_defined_tools} } ) {
  my %tool = ( cmd => $_ );
  push @user_defined_tools, \%tool;
  add_user_defined_tool_menu( \%tool );
 }

 # 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{ocropus}     = check_command('ocroscript');
 $dependencies{cuneiform}   = check_command('cuneiform');
 $dependencies{djvu}        = check_command('cjb2');
 $dependencies{unpaper}     = check_command('unpaper');
 $dependencies{libtiff}     = check_command('tiffcp');
 if ( $dependencies{ocropus} ) {

  unless ( defined $ENV{OCROSCRIPTS} ) {
   for (qw(/usr /usr/local)) {
    $ENV{OCROSCRIPTS} = "$_/share/ocropus/scripts"
      if ( -d "$_/share/ocropus/scripts" );
   }
  }
  if ( defined $ENV{OCROSCRIPTS} ) {
   if ( -f "$ENV{OCROSCRIPTS}/recognize.lua" ) {
    $SETTING{ocroscript} = 'recognize';
   }
   elsif ( -f "$ENV{OCROSCRIPTS}/rec-tess.lua" ) {
    $SETTING{ocroscript} = 'rec-tess';
   }
   if ( defined $SETTING{ocroscript} ) {
    $logger->info("Using ocropus with $SETTING{ocroscript}.");
   }
   else {
    $logger->warn("Found ocropus, but no recognition scripts. Disabling.");
    $dependencies{ocropus} = 0;
   }
  }
  else {
   $logger->warn("Found ocropus, but not its scripts. Disabling.");
   $dependencies{ocropus} = 0;
  }
 }
 $logger->info("Found Image::Magick") if ( $dependencies{perlmagick} );
 $logger->info("Found ImageMagick")   if ( $dependencies{imagemagick} );
 $logger->info("Found scanadf")       if ( $dependencies{scanadf} );
 $logger->info("Found xdg-email")     if ( $dependencies{xdg} );
 $logger->info("Found gocr")          if ( $dependencies{gocr} );
 $logger->info("Found tesseract")     if ( $dependencies{tesseract} );
 $logger->info("Found cuneiform")     if ( $dependencies{cuneiform} );
 $logger->info("Found cjb2 (djvu)")   if ( $dependencies{djvu} );
 $logger->info("Found unpaper")       if ( $dependencies{unpaper} );
 $logger->info("Found libtiff")       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} );
 push @ocr_engine,
   [ 'ocropus', $d->get('Ocropus'), $d->get('Process image with Ocropus.') ],
   if ( $dependencies{ocropus} );
 push @ocr_engine,
   [
  'cuneiform', $d->get('Cuneiform'),
  $d->get('Process image with Cuneiform.')
   ],
   if ( $dependencies{cuneiform} );

 # 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('/MenuBar/File/Print')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Save')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Print')->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('/Thumb_Popup/Print')->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/User-defined')->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}
  and not $dependencies{ocropus}
  and not $dependencies{cuneiform} )
 {
  $uimanager->get_widget('/MenuBar/Tools/OCR')->set_sensitive(FALSE);
  $msg .= $d->get("OCR requires gocr, tesseract, ocropus, or cuneiform\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/unpaper',
  '/MenuBar/Tools/OCR',
  '/MenuBar/Tools/User-defined',

  '/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);
   }
   $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(TRUE);
   $uimanager->get_widget('/ToolBar/Print')->set_sensitive(TRUE);
   $uimanager->get_widget('/Thumb_Popup/Print')->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);
   }
   $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(FALSE);
   $uimanager->get_widget('/ToolBar/Print')->set_sensitive(FALSE);
   $uimanager->get_widget('/Thumb_Popup/Print')->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 ($@) {
  $logger->warn( $d->get('Warning: ') . "$@" );
  eval {
   $pixbuf =
     Gtk2::Gdk::Pixbuf->new_from_file_at_scale( $filename, $width, $height,
    TRUE );
  };
  $logger->info( sprintf( $d->get("Got %s on second attempt"), $filename ) )
    unless ($@);
 }

 return $pixbuf;
}

# Deletes all scans after warning.

sub new {

 # 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 clear all pages?")
   );
   if ( $response ne 'ok' ) {
    return FALSE;
   }
   else {
    last;
   }
  }
 }

 # 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 72ppi
 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 = $POINTS_PER_INCH;
 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);
 $logger->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;
}

# Create a file filter to show only supported file types in FileChooser dialog

sub add_filter {
 my ( $file_chooser, $name, @file_extensions ) = @_;
 my $filter = Gtk2::FileFilter->new;
 foreach (@file_extensions) {
  my @filter_pattern;
  my $byte;

  # Create case insensitive pattern
  foreach $byte ( split //, $_ ) {
   push( @filter_pattern, '[' . uc($byte) . lc($byte) . ']' );
  }
  my $new_filter_pattern = join( "", @filter_pattern );
  $filter->add_pattern( '*.' . $new_filter_pattern );
 }
 my $types;
 for (@file_extensions) {
  if ( defined $types ) {
   $types .= ", *.$_";
  }
  else {
   $types = "*.$_";
  }
 }
 $filter->set_name("$name ($types)");
 $file_chooser->add_filter($filter);
 $filter = Gtk2::FileFilter->new;
 $filter->add_pattern('*');
 $filter->set_name('All files');
 $file_chooser->add_filter($filter);
 return;
}

# 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');
 add_filter( $file_chooser, $d->get('gscan2pdf session files'), 'gs2p' );

 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) = @_;
 $logger->info("Restoring session in $SETTING{session}");
 if ( defined $filename ) {
  new();    # clear the page list
  my $tar = Archive::Tar->new( $filename, TRUE );
  my @filenamelist = $tar->list_files;
  $tar->extract;
 }
 my $sessionref = retrieve("$SETTING{session}/session");
 my %session    = %$sessionref;

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

  # Set up the canvas
  my $canvas;
  if ( defined $session{$page}{buffer} ) {
   $canvas = Goo::Canvas->new;
   my $root   = $canvas->get_root_item;
   my @bounds = @{ shift @{ $session{$page}{buffer} } };
   $canvas->set_bounds(@bounds);

   for my $box ( @{ $session{$page}{buffer} } ) {
    boxed_text( $root, $box->{text}, $box->{x1}, $box->{y1}, $box->{x2},
     $box->{y2}, $box->{height} );
   }
   $canvas->show;
  }

  # Populate the SimpleList
  push @{ $slist->{data} },
    [
   $page,
   get_pixbuf( $session{$page}{filename}, $heightt, $widtht ),
   $session{$page}{filename},
   $canvas, $session{$page}{resolution}
    ];
 }
 $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
 $slist->select(@selection);
 update_uimanager();
 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');
 add_filter( $file_chooser, $d->get('Image files'),
  'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps' );

 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 {

    $logger->info( "child LC_NUMERIC ", setlocale(LC_NUMERIC) );
    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;
      $logger->info( "parent LC_NUMERIC ", setlocale(LC_NUMERIC) );
      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 ) = @_;

 $logger->info("Importing $filename");

 # 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\"";
  $logger->info($cmd);
  my $info = `$cmd`;
  $logger->info($info);

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

  # Dig out and the resolution of each page
  my @ppi;
  while ( $info =~ /\s(\d+)\s+dpi/ ) {
   push @ppi, $1;
   $logger->info("Page $#ppi is $ppi[$#ppi] ppi");
   $info = substr( $info, index( $info, " dpi" ) + 4, length($info) );
  }
  if ( $last != @ppi ) {
   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";
     $logger->info($cmd);
     system($cmd);
     send(
      $parent,
      sprintf( "%f %s %i ", ( $i / $last + $j ) / $n, $tif, $ppi[ $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);
 $logger->warn($x) if "$x";

 my $format = $image->Get('format');
 $logger->info("Format $format") if ( 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\"`;
  $logger->info($info);
  if ( $info =~ /Pages:\s+(\d+)/ ) {
   my $last = $1;
   $logger->info("$last pages");
   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\"";
  $logger->info($cmd);
  my $info = `$cmd`;
  $logger->info($info);

  # Count number of pages and their resolutions
  my @ppi;
  while ( $info =~ /Resolution: (\d*)/ ) {
   push @ppi, $1;
   $info = substr( $info, index( $info, 'Resolution' ) + 10, length($info) );
  }
  my $last = @ppi;
  $logger->info("$last pages");
  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";
    $logger->info($cmd);
    system($cmd);
    send(
     $parent,
     sprintf( "%f %s %i ",
      ( ( $i - $first + 1 ) / ( $last - $first + 1 ) + $j ) / $n, $tif,
      $ppi[$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; } );
  $window_new->signal_connect(
   key_press_event => sub {
    my ( $widget, $event ) = @_;
    return unless $event->keyval == $Gtk2::Gdk::Keysyms{Escape};
    $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->signal_connect(
   key_press_event => sub {
    my ( $widget, $event ) = @_;
    return unless $event->keyval == $Gtk2::Gdk::Keysyms{Escape};
    $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, $mua_string ) = @_;

 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);
    $logger->warn($x) if "$x";

    # Get the size and resolution. Resolution is dots per inch, width
    # and height are in inches.
    my $resolution = $slist->{data}[$_][4];
    my $w          = $image->Get('width') / $resolution;
    my $h          = $image->Get('height') / $resolution;

    # The output resolution is normally the same as the input
    # resolution.
    my $output_resolution = $resolution;

    # Automatic mode
    my $depth;
    my $compression;
    my $type;
    if ( $SETTING{'pdf compression'} eq 'auto' ) {
     $depth = $image->Get('depth');
     $logger->info("Depth of $filename is $depth");
     if ( $depth == 1 ) {
      $compression = 'lzw';
     }
     else {
      $type = $image->Get('type');
      $logger->info("Type of $filename is $type");
      if ( $type =~ /TrueColor/ ) {
       $compression = 'jpg';
      }
      else {
       $compression = 'png';
      }
     }
     $logger->info("Selecting $compression compression");
    }
    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' ) {
      my $ofn = $filename;
      ( undef, $filename ) =
        tempfile( DIR => $SETTING{session}, SUFFIX => '.tif' );
      $logger->info("Converting $ofn to $filename");
     }
     elsif ( $compression =~ /(jpg|png)/ ) {
      my $ofn = $filename;
      ( undef, $filename ) = tempfile(
       DIR    => $SETTING{session},
       SUFFIX => ".$compression"
      );
      $logger->info("Converting $ofn to $filename");
     }

     $depth = $image->Get('depth') if ( !defined $depth );
     if ( $SETTING{'downsample'} ) {
      $output_resolution = $SETTING{'downsample dpi'};
      my $w_pixels = $w * $output_resolution;
      my $h_pixels = $h * $output_resolution;

      $logger->info("Resizing $filename to $w_pixels x $h_pixels");
      $x = $image->Resize( width => $w_pixels,, height => $h_pixels );
      $logger->warn($x) if "$x";
     }
     $x = $image->Set( quality => $SETTING{quality} )
       if ( $compression eq 'jpg' );
     $logger->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
      $logger->info("Writing temporary image $filename with depth $depth");
      $x = $image->Write( filename => $filename, depth => $depth );
      $logger->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";
      $logger->info($cmd);
      my $status = system("$cmd 2>$SETTING{session}/tiffcp.stdout");
      if ( $status != 0 ) {
       my $output = slurp("$SETTING{session}/tiffcp.stdout");
       $logger->info($output);
       send( $parent,
        '-1' . sprintf( $d->get("Error compressing image: %s"), $output ), 0 );
      }
      $filename = $filename2;
     }
    }

    $logger->info(
     "Defining page at ",
     $w * $POINTS_PER_INCH,
     "pt x ", $h * $POINTS_PER_INCH, "pt"
    );
    my $page = $pdf->page;
    $page->mediabox( $w * $POINTS_PER_INCH, $h * $POINTS_PER_INCH );

    # Add OCR as text behind the scan
    if ( defined( $slist->{data}[$_][3] ) ) {
     $logger->info("Embedding OCR output behind image");
     my $font   = $pdf->corefont('Times-Roman');
     my $text   = $page->text;
     my $canvas = $slist->{data}[$_][3];
     my $root   = $canvas->get_root_item;
     my $n      = $root->get_n_children;
     for ( my $i = 0 ; $i < $n ; $i++ ) {
      my $group = $root->get_child($i);
      if ( $group->isa('Goo::Canvas::Group') ) {
       my $bounds = $group->get_bounds;
       my ( $x1, $y1, $x2, $y2 ) =
         ( $bounds->x1 + 1, $bounds->y1 + 1, $bounds->x2 - 1, $bounds->y2 - 1 );
       my $n = $group->get_n_children;
       for ( my $i = 0 ; $i < $n ; $i++ ) {
        my $item = $group->get_child($i);
        if ( $item->isa('Goo::Canvas::Text') ) {
         if ( abs( $h * $resolution - $y2 + $y1 ) > 5
          and abs( $w * $resolution - $x2 + $x1 ) > 5 )
         {

          # Box is smaller than the page. We know the text position.
          # Set the text position.
          # Translate x1 and y1 to inches and then to points. Invert the
          # y coordinate (since the PDF coordinates are bottom to top
          # instead of top to bottom) and subtract $size, since the text
          # will end up above the given point instead of below.
          my $size = ( $y2 - $y1 ) / $resolution * $POINTS_PER_INCH;
          $text->font( $font, $size );
          $text->translate( $x1 / $resolution * $POINTS_PER_INCH,
           ( $h - ( $y1 / $resolution ) ) * $POINTS_PER_INCH - $size );
          $text->text( $item->get('text') );
         }
         else {

          # Box is the same size as the page. We don't know the text position.
          # Start at the top of the page (PDF coordinate system starts
          # at the bottom left of the page)
          my $size = 1;
          $text->font( $font, $size );
          my $y = $h * $POINTS_PER_INCH;
          foreach my $line ( split( "\n", $item->get('text') ) ) {
           my $x = 0;

           # Add a word at a time in order to linewrap
           foreach my $word ( split( ' ', $line ) ) {
            if ( length($word) * $size + $x > $w * $POINTS_PER_INCH ) {
             $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 = "Unknown format $format file $filename";
    }
    if ($msg) {
     $logger->warn($msg);
     send( $parent,
      '-1' . sprintf( $d->get("Error creating PDF image object: %s"), $msg ),
      0 );
    }
    else {
     eval {
      $gfx->image( $imgobj, 0, 0, $w * $POINTS_PER_INCH,
       $h * $POINTS_PER_INCH );
     };
     if ($@) {
      $logger->warn($@);
      send(
       $parent,
       '-1'
         . sprintf(
        $d->get("Error embedding file image in %s format to PDF: %s"),
        $format, $@
         ),
       0
      );
     }
     else {
      $logger->info("Adding $filename at $output_resolution PPI");
     }
    }
   }
   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);

      # create email if required
      if ( defined $mua_string ) {
       show_message_dialog( $window, 'error', 'close',
        $d->get('Error creating email') )
         if ( system($mua_string) );
      }

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

# Draw text on the canvas with a box around it

sub boxed_text {
 my ( $root, $char, $x1, $y1, $x2, $y2, $size ) = @_;
 my $g = Goo::Canvas::Group->new($root);
 $g->translate( $x1, $y1 );

# draw the rect first to make sure the text goes on top and receives any mouse clicks
 my $rect = Goo::Canvas::Rect->new( $g, 0, 0, $x2 - $x1, $y2 - $y1 );
 my $text =
   Goo::Canvas::Text->new( $g, $char, 0, 0, $x2 - $x1, 'nw', 'height' => $size,
   );

 # clicking text box produces a dialog to edit the text
 $text->signal_connect(
  'button-press-event' => sub {
   my ( $widget, $target, $ev ) = @_;
   my $dialog = Gtk2::Dialog->new(
    $d->get('Editing text') . "...", $window,
    'modal',
    'gtk-ok'     => 'ok',
    'gtk-cancel' => 'cancel'
   );
   my $textview   = Gtk2::TextView->new;
   my $textbuffer = $textview->get_buffer;
   my $char       = $widget->get('text');
   $textbuffer->set( text => $char );
   $dialog->vbox->add($textview);
   $dialog->set_default_response('ok');
   $dialog->show_all;

   if ( $dialog->run eq 'ok' ) {
    my $char = $textbuffer->get('text');
    $widget->set( text => $char );
   }
   $dialog->destroy;
   $canvas->pointer_ungrab( $widget, $ev->time );
   return TRUE;
  }
 );
# $rect->signal_connect(
#  'button-press-event' => sub {
#   my ( $widget, $target, $ev ) = @_;
#   print "rect button-press-event\n";
#   #  return TRUE;
#  }
# );
# $g->signal_connect(
#  'button-press-event' => sub {
#   my ( $widget, $target, $ev ) = @_;
#   print "group $widget button-press-event\n";
#   my $n = $widget->get_n_children;
#   for ( my $i = 0 ; $i < $n ; $i++ ) {
#    my $item = $widget->get_child($i);
#    if ( $item->isa('Goo::Canvas::Text') ) {
#     print "contains $item\n", $item->get('text'), "\n";
#     last;
#    }
#   }
#   #  return TRUE;
#  }
# );
 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'
 );

 my ( $dday, $dmonth, $dyear ) =
   ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
 $dyear += 1900;
 $dmonth = sprintf( "%02d", $dmonth + 1 );
 $dday   = sprintf( "%02d", $dday );
 my ( $tday, $tmonth, $tyear ) = ( localtime(time) )[ 3, 4, 5 ];
 $tyear += 1900;
 $tmonth = sprintf( "%02d", $tmonth + 1 );
 $tday   = sprintf( "%02d", $tday );

 my $filename = $SETTING{'default filename'};
 $filename =~ s/%a/$SETTING{author}/g;
 $filename =~ s/%t/$SETTING{title}/g;
 $filename =~ s/%y/$dyear/g;
 $filename =~ s/%Y/$tyear/g;
 $filename =~ s/%m/$dmonth/g;
 $filename =~ s/%M/$tmonth/g;
 $filename =~ s/%d/$dday/g;
 $filename =~ s/%D/$tday/g;

 $file_chooser->set_current_name($filename);
 $file_chooser->set_default_response('ok');
 add_filter( $file_chooser, $d->get('PDF files'), 'pdf' );
 $file_chooser->set_do_overwrite_confirmation(TRUE);
 $file_chooser->signal_connect(
  response => sub {
   my ( $dialog, $response ) = @_;
   $logger->debug("PDF save filename dialog returned $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) or ( -f $filename and !-w $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
    $logger->debug("Started saving $filename");
    create_PDF($filename);
    $logger->debug("Finished saving $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('PPI') );
 $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 and 4 give an error message
#  [ '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'
    or $type[ $combobi->get_active ][0] eq 'ps' )
   {
    $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');
    add_filter( $file_chooser, $d->get('Image files'), $SETTING{'image type'} );
    $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) or ( -f $filename and !-w $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);
    add_filter( $file_chooser, $d->get('Text files'), 'txt' );
    $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) or ( -f $filename and !-w $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++ ) {
        my $canvas = $slist->{data}[ $pagelist[$i] ][3];
        my $root   = $canvas->get_root_item;
        my $n      = $root->get_n_children;
        for ( my $i = 0 ; $i < $n ; $i++ ) {
         my $group = $root->get_child($i);
         if ( $group->isa('Goo::Canvas::Group') ) {
          my $n = $group->get_n_children;
          for ( my $i = 0 ; $i < $n ; $i++ ) {
           my $item = $group->get_child($i);
           printf $fh "%s\n", $item->get('text')
             if ( $item->isa('Goo::Canvas::Text') );
          }
         }
        }
       }
       close $fh;
       $windowi->hide if defined $windowi;
      }
      $file_chooser->destroy;
     }
    );
    $file_chooser->show;

    # cd back to tempdir
    chdir $SETTING{session};
   }
   elsif ( $SETTING{'image type'} eq 'ps' ) {
    $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('PS filename'),
     $windowi, 'save',
     'gtk-cancel' => 'cancel',
     'gtk-save'   => 'ok'
    );
    $file_chooser->set_default_response('ok');
    add_filter( $file_chooser, $d->get('Postscript files'), 'ps' );
    $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) or ( -f $filename and !-w $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');
    add_filter( $file_chooser, $d->get('gscan2pdf session files'), 'gs2p' );
    $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) or ( -f $filename and !-w $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'
  and $type[ $combobi->get_active ][0] ne 'ps' );
 $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) or ( -f $filename and !-w $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');
 add_filter( $file_chooser, $d->get('Image files'),
  'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps' );

 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'";
    $logger->info($cmd);
    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;
   my $w = length( scalar @filelist );
   my $current_filename;
   foreach (@filelist) {
    $current_filename = sprintf "${filename}_%0${w}d.$SETTING{'image type'}",
      $i;
    if ( !file_exists( $file_chooser, $current_filename ) ) {
     my $cmd = sprintf "convert %s -density %d \"%s\"",
       $_, $slist->{data}[ $pagelist[ $i - 1 ] ][4],
       $current_filename;
     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\"";
   $logger->info($cmd);
   my $status = system("$cmd 2>$SETTING{session}/tiffcp.stdout");
   my $output = '';
   if ( $status != 0 ) {
    $output = slurp("$SETTING{session}/tiffcp.stdout");
    $logger->info($output);
    send( $parent,
     '-1' . sprintf( $d->get("Error compressing image: %s"), $output ), 0 );
   }
   if ( defined $ps ) {
    send( $parent, '1' . $d->get('Converting to PS'), 0 );

    # Note: -a option causes tiff2ps to generate multiple output
    # pages, one for each page in the input TIFF file.  Without it, it
    # only generates output for the first page.
    my $cmd = "tiff2ps -a $filename > \"$ps\"";
    $logger->info($cmd);
    $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');
 add_filter( $file_chooser, $d->get('DjVu files'), 'djvu' );
 $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) or ( -f $filename and !-w $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);
       $logger->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 );
         $logger->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 );
         $logger->warn($x) if "$x";
         $filename = $pbm;
        }
       }

       # Create the djvu
       my $resolution = $slist->{data}[$j][4];
       my $cmd        = "$compression -dpi $resolution $filename $djvu";
       $logger->info($cmd);
       my ( $status, $size ) = ( system($cmd), -s $djvu );
       unless ( $status == 0 and $size ) {
        send( $parent, "2" . $d->get('Error writing DjVu'), 0 );
        $logger->error( "Error writing image for page ",
         $i + 1, " of DjVu (process returned $status, image size $size)" );
       }
       $filelist[$i] = $djvu;

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

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

        # Open djvusedtxtfile
        my ( undef, $djvusedtxtfile ) =
          tempfile( DIR => $SETTING{session}, SUFFIX => '.txt' );
        open my $fh, ">:utf8", $djvusedtxtfile
          or die sprintf( $d->get("Can't open file: %s"), $djvusedtxtfile );
        print $fh "(page 0 0 $w $h\n";

        # Write the text boxes
        my $canvas = $slist->{data}[$j][3];
        my $root   = $canvas->get_root_item;
        my $n      = $root->get_n_children;
        for ( my $i = 0 ; $i < $n ; $i++ ) {
         my $group = $root->get_child($i);
         if ( $group->isa('Goo::Canvas::Group') ) {
          my $n      = $group->get_n_children;
          my $bounds = $group->get_bounds;
          my ( $x1, $y1, $x2, $y2 ) = (
           $bounds->x1 + 1,
           $bounds->y1 + 1,
           $bounds->x2 - 1,
           $bounds->y2 - 1
          );
          for ( my $i = 0 ; $i < $n ; $i++ ) {
           my $item = $group->get_child($i);
           if ( $item->isa('Goo::Canvas::Text') ) {

            # Escape any inverted commas
            my $txt = $item->get('text');
            $txt =~ s/\\/\\\\/g;
            $txt =~ s/"/\\\"/g;
            printf $fh "\n(line %d %d %d %d \"%s\")", $x1, $h - $y2, $x2,
              $h - $y1, $txt;
           }
          }
         }
        }
        print $fh ")";
        close $fh;

        # Write djvusedtxtfile
        my $cmd = "djvused '$djvu' -e 'select 1; set-txt $djvusedtxtfile' -s";
        $logger->info($cmd);
        unless ( system($cmd) == 0 ) {
         send( $parent, "2" . $d->get('Error adding text layer to DjVu'), 0 );
         $logger->error( "Error adding text layer to DjVu page ", $i + 1 );
        }
       }
      }
      send( $parent, '1' . $d->get('Closing DjVu'), 0 );
      my $cmd = "djvm -c '$filename' @filelist";
      $logger->info($cmd);
      unless ( system($cmd) == 0 ) {
       send( $parent, "2" . $d->get('Error closing DjVu'), 0 );
       $logger->error("Error closing DjVu");
      }

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

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

   my $mua_string;
   if ( $client =~ /thunderbird/ ) {
    $mua_string = "thunderbird -compose attachment=file://$pdf";
   }
   else {
    $mua_string = "xdg-email --attach $pdf 'x\@y'";
   }

   # Create the PDF
   create_PDF( $pdf, $mua_string );

   $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}
  and not $dependencies{ocropus}
  and not $dependencies{cuneiform} )
 {
  $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 ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );
 if ( $dependencies{tesseract} ) {
  ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vboxp);
  $comboboxe->signal_connect(
   changed => sub {
    if ($ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
    {
     $hboxtl->show_all;
    }
    else {
     $hboxtl->hide_all;
    }
   }
  );
  if ( $dependencies{cuneiform} ) {
   ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vboxp);
   $comboboxe->signal_connect(
    changed => sub {
     if ( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) {
      $hboxcl->show_all;
     }
     else {
      $hboxcl->hide_all;
     }
    }
   );
  }
  $hboxtl->set_sensitive(FALSE) if ( !( $obutton->get_active ) );
  $obutton->signal_connect(
   toggled => sub {
    if ( $obutton->get_active ) {
     $hboxtl->set_sensitive(TRUE);
    }
    else {
     $hboxtl->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];
     }
    }
   }
   $logger->info("rotate facing $SETTING{'rotate facing'}");
   $logger->info("rotate reverse $SETTING{'rotate reverse'}");
   my $rotate_facing  = $SETTING{'rotate facing'};
   my $rotate_reverse = $SETTING{'rotate reverse'};
   $logger->info( $duplex ? "" : "non-", "duplex mode" );
   if ( !$duplex ) {
    if ( $step > 0 ) {
     $rotate_reverse = $SETTING{'rotate facing'};
    }
    else {
     $rotate_facing = $SETTING{'rotate reverse'};
    }
   }
   $logger->info("rotate_facing $rotate_facing");
   $logger->info("rotate_reverse $rotate_reverse");

   $SETTING{'unpaper on scan'} = $ubutton->get_active;
   $logger->info("unpaper $SETTING{'unpaper on scan'}");
   $SETTING{'OCR on scan'} = $obutton->get_active;
   $logger->info("OCR $SETTING{'OCR on scan'}");
   if ( $SETTING{'OCR on scan'} ) {
    $SETTING{'ocr engine'}   = $ocr_engine[ $comboboxe->get_active ]->[0];
    $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
      if ( $SETTING{'ocr engine'} eq 'tesseract'
     or $SETTING{'ocr engine'} eq 'ocropus' );
    $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
      if ( $SETTING{'ocr engine'} eq 'cuneiform' );
   }
   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;
 $hboxtl->hide_all
   if (
  defined($hboxtl)
  and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
   or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
   );
 $hboxcl->hide_all
   if ( defined($hboxcl)
  and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) );

# Has to be done after showing the window, otherwise the window doesn't get centred.
 if ($test) {
  $sbutton->grab_focus;
 }
 else {
  $sbutton->set_sensitive(FALSE);
 }
 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";
 $logger->info($cmd);

 # Interface to frontend
 my $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
 $logger->info("Forked PID $pid");

 # 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;
    $logger->info('Waiting to reap process');
    my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
    $logger->info("Reaped PID $pid");
    $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}
   or $dependencies{tesseract}
   or $dependencies{ocropus}
   or $dependencies{cuneiform} )
   )
 {
  $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 ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );
 if ( $dependencies{tesseract} ) {
  ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vboxp);
  $comboboxe->signal_connect(
   changed => sub {
    if ($ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
    {
     $hboxtl->show_all;
    }
    else {
     $hboxtl->hide_all;
    }
   }
  );
  if ( $dependencies{cuneiform} ) {
   ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vboxp);
   $comboboxe->signal_connect(
    changed => sub {
     if ( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) {
      $hboxcl->show_all;
     }
     else {
      $hboxcl->hide_all;
     }
    }
   );
  }
  $hboxtl->set_sensitive(FALSE) if ( !( $obutton->get_active ) );
  $obutton->signal_connect(
   toggled => sub {
    if ( $obutton->get_active ) {
     $hboxtl->set_sensitive(TRUE);
    }
    else {
     $hboxtl->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];
     }
    }
   }
   $logger->info("rotate facing $SETTING{'rotate facing'}");
   $logger->info("rotate reverse $SETTING{'rotate reverse'}");
   my $rotate_facing  = $SETTING{'rotate facing'};
   my $rotate_reverse = $SETTING{'rotate reverse'};
   $logger->info( $duplex ? "" : "non-", "duplex mode" );
   if ( !$duplex ) {
    if ( $step > 0 ) {
     $rotate_reverse = $SETTING{'rotate facing'};
    }
    else {
     $rotate_facing = $SETTING{'rotate reverse'};
    }
   }
   $logger->info("rotate_facing $rotate_facing");
   $logger->info("rotate_reverse $rotate_reverse");

   $SETTING{'unpaper on scan'} = $ubutton->get_active;
   $logger->info("unpaper $SETTING{'unpaper on scan'}");
   $SETTING{'OCR on scan'} = $obutton->get_active;
   $logger->info("OCR $SETTING{'OCR on scan'}");
   if ( $SETTING{'OCR on scan'} ) {
    $SETTING{'ocr engine'}   = $ocr_engine[ $comboboxe->get_active ]->[0];
    $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
      if ( $SETTING{'ocr engine'} eq 'tesseract'
     or $SETTING{'ocr engine'} eq 'ocropus' );
    $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
      if ( $SETTING{'ocr engine'} eq 'cuneiform' );
   }
   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;
 $hboxtl->hide_all
   if (
  defined($hboxtl)
  and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
   or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
   );
 $hboxcl->hide_all
   if ( defined($hboxcl)
  and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) );

 # Has to be done in idle cycles to wait for the options to finish building
 Glib::Idle->add( sub { $sbutton->grab_focus; } );
 return;
}

# Run Sane->get_devices

sub get_devices2 {
 my $hboxd = shift;

 Gscan2pdf::Backend::Sane->get_devices(
  $hboxd,
  sub {
   my ($data) = @_;
   my @old_device_list = @device_list;
   @device_list = @{$data};
   $logger->info( "Sane->get_devices returned: ", Dumper( \@device_list ) );
   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);
   }
  }
 );
 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};
     }
    }
   }
  }
 }
 $logger->debug( Dumper( \%options ) );
 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  = ();

 $logger->info($output) if ( 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";
 $logger->info($cmd);

 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 );
  $logger->info("Forked PID $pid");

  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';
    $logger->info("Sending INT signal to PID $pid and its children");
    killfam '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;
     $logger->debug($buffer) if ($buffer);
     $line .= $buffer;

     while ( $line =~ /([\r\n])/ ) {
      my $le = $1;
      if ( $line =~ /^Progress: (\d*\.\d*)%/ ) {
       my $fraction = $1 / 100;
       $pbar->set_fraction($fraction);
      }
      elsif ( $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\)/ ) {
       my $id = $1;

       # If the scan can't be loaded then blow the scanning dialog away and
       # show an error
       my $rotate = $id % 2 ? $rfacing : $rreverse;

       # Timer will run until callback returns false
       my $timer = Glib::Timeout->add(
        100,
        sub {
         return TRUE unless ( -e "out$id.pnm" );
         my $index = import_scan( "out$id.pnm", $id * $step + $offset,
          $SETTING{resolution}, TRUE, $rotate, $unpaper, $ocr );
         $num_scans++;
         return FALSE;
        }
       );
      }
      elsif ( $line =~
       /Scanner warming up - waiting \d* seconds|wait for lamp warm-up/ )
      {
       $pbar->set_text( $d->get('Scanner warming up') );
      }
      elsif ( $line =~ /^Scanned page \d*\. \(scanner status = 7\)/ ) {
       ;
      }
      elsif ( $line =~
       /^$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/ ) {
       $logger->info( 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, $le ) + 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;
     $logger->info('Waiting to reap process');
     my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
     $logger->info("Reaped PID $pid");

     # 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 $rotate = 1 % 2 ? $rfacing : $rreverse;
  my $index = import_scan( $test_image, 1 * $step + $offset,
   $SETTING{resolution}, FALSE, $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";
 $logger->info($cmd);

 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 );
  $logger->info("Forked PID $pid");

  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';
    $logger->info("Sending HUP signal to PID $pid and its children");
    killfam '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;
     $logger->debug($buffer) if ($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 $rotate = $id % 2 ? $rfacing : $rreverse;

       # Timer will run until callback returns false
       my $timer = Glib::Timeout->add(
        100,
        sub {
         return TRUE unless ( -e "out$id.pnm" );
         my $index = import_scan( "out$id.pnm", $id * $step + $offset,
          $SETTING{resolution}, TRUE, $rotate, $unpaper, $ocr );
         $pbar->set_text(
          sprintf( $d->get('Scanning page %i...'), $id * $step + $offset ) );
         $id++;
         return FALSE;
        }
       );
      }
      elsif ( $line =~ /^Scanned \d* pages/ ) {
       ;
      }
      elsif ( $line =~ /^$SETTING{frontend}: rounded/ ) {
       $logger->info( 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;
     $logger->info('Waiting to reap process');
     my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
     $logger->info("Reaped PID $pid");

     # 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 {
  $logger->info($cmd);
 }
 return;
}

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

 Gscan2pdf::Backend::Sane->scan_pages(
  $windows2,
  $format, $npages, $n, $step,
  sub {
   my ($n) = @_;

   # If the scan can't be loaded then blow the scanning dialog away and
   # show an error
   my $rotate = $n % 2 ? $rfacing : $rreverse;
   $SETTING{resolution} =
     defined( $SETTING{resolution} )
     ? $SETTING{resolution}
     : $POINTS_PER_INCH;
   $logger->info("Importing scan with resolution=$SETTING{resolution}");
   my $index =
     import_scan( File::Spec->catdir( $SETTING{session}, "out$n.pnm" ),
    $n, $SETTING{resolution}, TRUE, $rotate, $unpaper, $ocr );
  },
  sub {
   my ($msg) = @_;
   show_message_dialog( $windows2, 'error', 'close', $d_sane->get($msg) );
  }
 );
 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;
 $logger->info("OCRflagging page: $page") if $ocr;
 $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, $resolution, $delete, $rotate, $unpaper, $ocr ) = @_;

 # Interface to frontend
 open my $fh, '<', $ofilename or die "can't open $ofilename: $!";

 # Read without blocking
 my $size = 0;
 Glib::IO->add_watch(
  fileno($fh),
  [ 'in', 'hup' ],
  sub {
   my ( $fileno, $condition ) = @_;
   if ( $condition & 'in' ) {    # bit field operation. >= would also work
    if ( $size == 0 ) {
     my @data;
     while (<$fh>) {
      $size += length;
      next if (/^#/);
      my @cols = split;
      @data = ( @data, @cols );
      my $n = 4;
      $n = 3 if ( $data[0] eq 'P4' );
      last if ( @data == $n );
     }
     $logger->info("Format is $data[0]");
     $logger->info("Header is $size");
     my ( $format, $width, $height ) = @data;
     if ( $format eq 'P4' ) {
      $size += $width * $height / 8;
     }
     elsif ( $format eq 'P5' ) {
      $size += $width * $height;
     }
     elsif ( $format eq 'P6' ) {
      $size += $width * $height * 3;
     }
     else {
      $logger->error("Unknown format $format");
     }
     $logger->info("Header suggests $size");
     close($fh);
    }
    $logger->info( sprintf "Expecting $size, found %d", -s $ofilename );
    if ( $size <= -s $ofilename ) {
     ( my $filename, $resolution ) =
       prepare_import( $ofilename, 'Portable anymap', $resolution, $delete );
     my $index = add_image( $filename, $page, $resolution );
     if ( $index == -1 ) {
      show_message_dialog( $windows, 'error', 'close',
       $d->get('Unable to load image') );
     }
     else {
      post_process_scan( $index, $rotate, $unpaper, $ocr );
     }
     return FALSE;    # uninstall
    }
   }
   return TRUE;       # continue without uninstalling
  }
 );

 return;
}

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

 $logger->info("Importing $ofilename, format $format");

 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);
  $logger->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) );
 $logger->info("Added $filename at page $page with resolution $resolution");

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

# print

sub print_dialog {
 chdir $SETTING{'cwd'};
 my $op = Gtk2::PrintOperation->new;

 $op->set_print_settings($print_settings)
   if ( defined $print_settings );

 $op->signal_connect(
  begin_print => sub {
   my ( $op, $context ) = @_;

   my $print_settings = $op->get_print_settings;
   my $pages          = $print_settings->get('print-pages');
   my @page_list;
   if ( $pages eq 'ranges' ) {
    my $page_set = Set::IntSpan->new;
    my $ranges   = $print_settings->get('page-ranges');
    for ( split ',', $ranges ) {
     $page_set->I($_);
    }
    for ( 0 .. $#{ $slist->{data} } ) {
     push @page_list, $_ if ( $page_set->member( $slist->{data}[$_][0] ) );
    }
   }
   else {
    @page_list = ( 0 .. $#{ $slist->{data} } );
   }
   $op->set_n_pages( scalar(@page_list) );
  }
 );

 $op->signal_connect(
  draw_page => sub {
   my ( $op, $context, $page_number ) = @_;

   my $cr = $context->get_cairo_context;

   # Context dimensions
   my $pwidth  = $context->get_width;
   my $pheight = $context->get_height;

   # Image dimensions
   my $pixbuf =
     Gtk2::Gdk::Pixbuf->new_from_file( $slist->{data}[$page_number][2] );
   my $iwidth  = $pixbuf->get_width;
   my $iheight = $pixbuf->get_height;

   # Scale context to fit image
   my $scale = $pwidth / $iwidth;
   $scale = $pheight / $iheight if ( $pheight / $iheight < $scale );
   $cr->scale( $scale, $scale );

   # Set source pixbuf
   Gtk2::Gdk::Cairo::Context::set_source_pixbuf( $cr, $pixbuf, 0, 0 );

   # Paint
   $cr->paint;

   return;
  }
 );

 my $res = $op->run( 'print-dialog', $window );

 $print_settings = $op->get_print_settings if ( $res eq 'apply' );
 chdir $SETTING{session};
 return;
}

# 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);
  $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
 }

 # 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} } ) {
  $logger->info("smp page: $page");
  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 ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analyzed.  Try Update or Analyze.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Std Dev to threshold
  if ( $slist->{data}[$page][7] <= $SETTING{'Blank threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting blank page");
  }
  $logger->info( "StdDev: "
     . $slist->{data}[$page][7]
     . " threshold: "
     . $SETTING{'Blank threshold'} );
 }
 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;
 $logger->info("Checking for dark pages now, is analysis done?");
 foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  $logger->info("smp page: $page");
  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 ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analyzed.  Try Update or Analyze.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Mean to threshold
  if ( $slist->{data}[$page][6] <= $SETTING{'Dark threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting dark page");
  }
 }
 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 ) {
   $logger->info(
    "Updating: $page analyse_time: $analyze_time dirty_time: $dirty_time");
   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;
}

# Display about dialog

sub about {
 use utf8;
 my $about = Gtk2::AboutDialog->new;

 # Gtk2::AboutDialog->set_url_hook ($func, $data=undef);
 # Gtk2::AboutDialog->set_email_hook ($func, $data=undef);
 $about->set_program_name($prog_name);
 $about->set_version($version);
 my $authors = <<EOS;
John Goerzen
Chris Mayo
David Hampton
Sascha Hunold
Jakub Wilk
Frederik Elwert
Roy Shahbazian
Klaus Ethgen
Matthijs Kooijman
Jarl Stefansson
Andy Fingerhut
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--2011 Jeffrey Ratcliffe') );
 my $licence = <<EOS;
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright 2006 -- 2011 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'; # inverted commas requred around EOS because of UTF-8 in $translators
Norbux
Oleg Koptev
Сергій Дубик
Matthias Gutjahr
Fitoschido
YAMAKAGE Hideo
Alejandro Cuervo
Tico
suoko
Piotr Strębski
JeromeJ
David Planella
Manuel Rennecke
Wojciech Górnaś
Sérgio Marques
Cédric VALMARY
Celso H. L. S. Junior
Jarosław Ogrodnik
Luisa Ravelli
John M
EOS
 utf8::encode($translators);    # UTF-8 in perl sucks!
 $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} ) {
  my %options  = options2hash( $SETTING{cache}{$device}{$mode} );
  my $response = 'cancel';
  if ( not %options ) {
   $response = show_message_dialog(
    $window,
    'question',
    'ok-cancel',
    $d->get(
     "The options cache seems to be empty. Reload device-dependent options?")
   );
  }
  if ( $response eq 'cancel' ) {
   parse_options( $vboxd, $device, %options );
   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} );
  $logger->info($cmd);

  # 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: $!";
  $logger->info("Forked PID $pid");

  # 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;
     $logger->info('Waiting to reap process');
     my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
     $logger->info("Reaped PID $pid");
     $running = FALSE;
     $pbar->destroy;
     $vboxd->hide_all;
     $logger->info($output);
     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
 if ( defined $sbutton ) {
  $sbutton->set_sensitive(TRUE);
  $sbutton->grab_focus;
 }
 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} ) {
  my %options  = options2hash( $SETTING{cache}{$device}{$mode} );
  my $response = 'cancel';
  if ( not %options ) {
   $response = show_message_dialog(
    $window,
    'question',
    'ok-cancel',
    $d->get(
     "The options cache seems to be empty. Reload device-dependent options?")
   );
  }
  if ( $response eq 'cancel' ) {
   update_options_hash( $vboxd, %options );
   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} );
  $logger->info($cmd);

  # 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: $!";
  $logger->info("Forked PID $pid");

  # 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;
     $logger->info('Waiting to reap process');
     my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
     $logger->info("Reaped PID $pid");
     $running = FALSE;
     $pbar->destroy;
     $vboxd->hide_all;
     $logger->info($output);
     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);
 $logger->debug( Dumper( \%hash ) );
 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 );

  Gscan2pdf::Backend::Sane->open_device(
   $device,
   $windows2,
   sub {
    $rotating = FALSE;
    kill_subs();
   },
   sub {
    Gscan2pdf::Backend::Sane->find_scan_options(
     $windows2,
     sub {
      $rotating = FALSE;
      kill_subs();
     },
     sub {
      my ($data) = @_;
      my @options = @{$data};
      $logger->debug( "Sane->get_option_descriptor returned: ",
       Dumper( \@options ) );

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

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

       # Note resolution default
       $SETTING{resolution} = $val
         if ( $opt->{name} eq SANE_NAME_SCAN_RESOLUTION );

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

       if ( $opt->{max_values} < 2 ) {

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

        # 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 );
          }
         );
        }
       }
       else {    # $opt->{max_values} > 1
        $widget = Gtk2::Button->new( $d_sane->get( $opt->{title} ) );
        $widget->{signal} = $widget->signal_connect(
         clicked => sub {
          if ($opt->{type} == SANE_TYPE_FIXED
           or $opt->{type} == SANE_TYPE_INT )
          {
           if ( $opt->{constraint_type} == SANE_CONSTRAINT_NONE ) {
            show_message_dialog(
             $windows2,
             'info', 'close',
             $d->get(
'Multiple unconstrained values are not currently supported. Please file a bug.'
             )
            );
           }
           else {
            set_options( $widget->{option_number}, $opt, @widgets );
           }
          }
          else {
           show_message_dialog(
            $windows2,
            'info', 'close',
            $d->get(
'Multiple non-numerical values are not currently supported. Please file a bug.'
            )
           );
          }
         }
        );
       }

       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 or $opt->{max_values} > 1 ) {
         $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 {
        $logger->warn("Unknown type $opt->{type}");
       }
      }

      # Set defaults
      my $sane_device = Gscan2pdf::Backend::Sane->device();

     # Move them first to a dummy array, as otherwise it would be self-modifying
      if ( defined $SETTING{default}{$sane_device} ) {

       # Config::General flattens arrays with 1 entry to scalars,
       # so we must check for this
       my @defaults;
       if ( ref( $SETTING{default}{$sane_device} ) ne 'ARRAY' ) {
        push @defaults, $SETTING{default}{$sane_device};
       }
       else {
        @defaults = @{ $SETTING{default}{$sane_device} };
       }
       delete $SETTING{default}{$sane_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] } );

          # Note resolution default
          $SETTING{resolution} = $val
            if ( $option eq SANE_NAME_SCAN_RESOLUTION );

          my $widget = $optnamei{$option};
          if ( ref($val) eq 'ARRAY' ) {
           set_option( $widget->{option_number}, $val, @widgets );

 # when INFO_INEXACT is implemented, so that the value is reloaded, check for it
 # here, so that the reloaded value is not overwritten.
           $options[ $widget->{option_number} ]->{val} = $val;
          }
          elsif ( $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);
      $sbutton->grab_focus;
     }
    );
   }
  );

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

  # Note resolution
  $SETTING{resolution} = $val
    if ( $widgets[$i]->{option_name} eq SANE_NAME_SCAN_RESOLUTION );

  my $sane_device = Gscan2pdf::Backend::Sane->device();

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

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

  Gscan2pdf::Backend::Sane->set_option(
   $windows2,
   $i, $val,
   sub {
    $rotating = FALSE;
    kill_subs();
   },
   sub {
    my ($data) = @_;

    if ($data) {

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

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

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

       if ( $opt->{max_values} < 2 ) {

        # 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} );
      }
     }
    }
    $gui_updating =
      FALSE;    # We can carry on applying defaults now, if necessary.
   }
  );
  return;
 }

 # display Goo::Canvas with graph

 sub set_options {
  my ( $i, $opt, @widgets ) = @_;

  # Set up the canvas
  my ( $window, $vbox ) =
    create_window( $windows2, $d_sane->get( $opt->{title} ), TRUE );
  my $canvas = Goo::Canvas->new;
  my ( $cwidth, $cheight ) = ( 200, 200 );
  $canvas->set_size_request( $cwidth, $cheight );
  $canvas->{border} = 10;
  $vbox->add($canvas);
  my $root = $canvas->get_root_item;

  $canvas->signal_connect(
   'button-press-event' => sub {
    my ( $canvas, $event ) = @_;
    if ( defined $canvas->{selected} ) {
     $canvas->{selected}->set( 'fill-color' => 'black' );
     undef $canvas->{selected};
    }
    return FALSE
      if ( $#{ $canvas->{val} } + 1 >= $opt->{max_values}
     or $canvas->{on_val} );
    my $fleur = Gtk2::Gdk::Cursor->new('fleur');
    my ( $x, $y ) = to_graph( $canvas, $event->x, $event->y );
    $x = int($x) + 1;
    splice @{ $canvas->{val} }, $x, 0, $y;
    splice @{ $canvas->{items} }, $x, 0, add_value( $root, $canvas );
    update_graph($canvas);
    return TRUE;
   }
  );

  $canvas->signal_connect_after(
   'key_press_event',
   sub {
    my ( $canvas, $event ) = @_;
    if ( $event->keyval == $Gtk2::Gdk::Keysyms{Delete}
     and defined $canvas->{selected} )
    {
     my $item = $canvas->{selected};
     undef $canvas->{selected};
     $canvas->{on_val} = FALSE;
     splice @{ $canvas->{val} },   $item->{index}, 1;
     splice @{ $canvas->{items} }, $item->{index}, 1;
     my $parent = $item->get_parent;
     my $num    = $parent->find_child($item);
     $parent->remove_child($num);
     update_graph($canvas);
    }
    return FALSE;
   }
  );
  $canvas->can_focus(TRUE);
  $canvas->grab_focus($root);

  $canvas->{opt} = $opt;

  $canvas->{val} = $canvas->{opt}->{val};
  for ( @{ $canvas->{val} } ) {
   push @{ $canvas->{items} }, add_value( $root, $canvas );
  }

  if ( $opt->{constraint_type} == SANE_CONSTRAINT_WORD_LIST ) {
   @{ $opt->{constraint} } = sort { $a <=> $b } @{ $opt->{constraint} };
  }

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

  # Apply button
  my $abutton = Gtk2::Button->new_from_stock('gtk-apply');
  $hbox->pack_start( $abutton, TRUE, TRUE, 0 );
  $abutton->signal_connect(
   clicked => sub {
    set_option( $i, $canvas->{val}, @widgets );

 # when INFO_INEXACT is implemented, so that the value is reloaded, check for it
 # here, so that the reloaded value is not overwritten.
    $opt->{val} = $canvas->{val};
    $window->destroy;
   }
  );

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

# Have to show the window before updating it otherwise is doesn't know how big it is
  $window->show_all;
  update_graph($canvas);
  return;
 }

 sub add_value {
  my ( $root, $canvas ) = @_;
  my $item = Goo::Canvas::Rect->new(
   $root, 0, 0, 10, 10,
   'fill-color' => 'black',
   'line-width' => 0,
  );
  $item->signal_connect(
   'enter-notify-event' => sub {
    $canvas->{on_val} = TRUE;
    return TRUE;
   }
  );
  $item->signal_connect(
   'leave-notify-event' => sub {
    $canvas->{on_val} = FALSE;
    return TRUE;
   }
  );
  $item->signal_connect(
   'button-press-event' => sub {
    my ( $widget, $target, $ev ) = @_;
    $canvas->{selected} = $item;
    $item->set( 'fill-color' => 'red' );
    my $fleur = Gtk2::Gdk::Cursor->new('fleur');
    $widget->get_canvas->pointer_grab( $widget,
     [ 'pointer-motion-mask', 'button-release-mask' ],
     $fleur, $ev->time );
    return TRUE;
   }
  );
  $item->signal_connect(
   'button-release-event' => sub {
    my ( $item, $target, $ev ) = @_;
    $item->get_canvas->pointer_ungrab( $item, $ev->time );
    return TRUE;
   }
  );
  my $opt = $canvas->{opt};
  $item->signal_connect(
   'motion-notify-event' => sub {
    my ( $item, $target, $event ) = @_;
    return FALSE unless ( $event->state >= 'button1-mask' );
    my ( $x, $y ) = ( $event->x, $event->y );
    my ( $xgr, $ygr ) = ( 0, $y );
    if ( $opt->{constraint_type} == SANE_CONSTRAINT_RANGE ) {
     ( $xgr, $ygr ) = to_graph( $canvas, 0, $y );
     if ( $ygr > $opt->{constraint}{max} ) {
      $ygr = $opt->{constraint}{max};
     }
     elsif ( $ygr < $opt->{constraint}{min} ) {
      $ygr = $opt->{constraint}{min};
     }
    }
    elsif ( $opt->{constraint_type} == SANE_CONSTRAINT_WORD_LIST ) {
     ( $xgr, $ygr ) = to_graph( $canvas, 0, $y );
     for ( my $i = 1 ; $i < @{ $opt->{constraint} } ; $i++ ) {
      if (
       $ygr < ( $opt->{constraint}[$i] + $opt->{constraint}[ $i - 1 ] ) / 2 )
      {
       $ygr = $opt->{constraint}[ $i - 1 ];
       last;
      }
      elsif ( $i == $#{ $opt->{constraint} } ) {
       $ygr = $opt->{constraint}[$i];
      }
     }
    }
    $canvas->{val}[ $item->{index} ] = $ygr;
    ( $x, $y ) = to_canvas( $canvas, $xgr, $ygr );
    $item->set( y => $y - 10 / 2 );
    return TRUE;
   }
  );
  return $item;
 }

 # convert from graph co-ordinates to canvas co-ordinates

 sub to_canvas {
  my ( $canvas, $x, $y ) = @_;
  return ( $x - $canvas->{bounds}[0] ) * $canvas->{scale}[0] +
    $canvas->{border},
    $canvas->{cheight} -
    ( $y - $canvas->{bounds}[1] ) * $canvas->{scale}[1] -
    $canvas->{border};
 }

 # convert from canvas co-ordinates to graph co-ordinates

 sub to_graph {
  my ( $canvas, $x, $y ) = @_;
  return ( $x - $canvas->{border} ) / $canvas->{scale}[0] +
    $canvas->{bounds}[0],
    ( $canvas->{cheight} - $y - $canvas->{border} ) / $canvas->{scale}[1] +
    $canvas->{bounds}[1];
 }

 sub update_graph {
  my ($canvas) = @_;

  # Calculate bounds of graph
  my @bounds;
  for ( @{ $canvas->{val} } ) {
   $bounds[1] = $_ if ( not defined $bounds[1] or $_ < $bounds[1] );
   $bounds[3] = $_ if ( not defined $bounds[3] or $_ > $bounds[3] );
  }
  my $opt = $canvas->{opt};
  $bounds[0] = 0;
  $bounds[2] = $#{ $canvas->{val} };
  if ( $bounds[0] >= $bounds[2] ) {
   $bounds[0] = -0.5;
   $bounds[2] = 0.5;
  }
  if ( $opt->{constraint_type} == SANE_CONSTRAINT_RANGE ) {
   $bounds[1] = $opt->{constraint}{min};
   $bounds[3] = $opt->{constraint}{max};
  }
  elsif ( $opt->{constraint_type} == SANE_CONSTRAINT_WORD_LIST ) {
   $bounds[1] = $opt->{constraint}[0];
   $bounds[3] = $opt->{constraint}[ $#{ $opt->{constraint} } ];
  }
  my ( $vwidth, $vheight ) =
    ( $bounds[2] - $bounds[0], $bounds[3] - $bounds[1] );

  # Calculate bounds of canvas
  my ( $x, $y, $cwidth, $cheight ) = $canvas->allocation->values;

  # Calculate scale factors
  my @scale = (
   ( $cwidth - $canvas->{border} * 2 ) / $vwidth,
   ( $cheight - $canvas->{border} * 2 ) / $vheight
  );

  $canvas->{scale}   = \@scale;
  $canvas->{bounds}  = \@bounds;
  $canvas->{cheight} = $cheight;

  # Update canvas
  for ( my $i = 0 ; $i <= $#{ $canvas->{items} } ; $i++ ) {
   my $item = $canvas->{items}[$i];
   $item->{index} = $i;
   my ( $x, $y ) = to_canvas( $canvas, $i, $canvas->{val}[$i] );
   $item->set( x => $x - 10 / 2, y => $y - 10 / 2 );
  }
  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);
    $logger->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);
    $logger->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 );
    $logger->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;
     $logger->info("index: $index $filename");
    }

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

    my ( $depth, $min, $max, $mean, $stddev ) = $image->Statistics();
    $logger->warn("image->Statistics() failed") unless defined $depth;
    $logger->info("std dev: $stddev mean: $mean");

#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' ) {
     $logger->info("index: $index depth: $depth") 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);
      $logger->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
      ( undef, $filename ) =
        tempfile( DIR => $SETTING{session}, SUFFIX => '.pbm' );
      $x = $image->Write( filename => $filename );
      $logger->warn($x) if "$x";
      $logger->info("Thresholding at $SETTING{'threshold tool'} to $filename");
      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);
      $logger->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 );
      $logger->warn($x) if "$x";
      $logger->info("Negating to $filename");
      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);
      $logger->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 );
      $logger->warn($x) if "$x";
      $logger->info(
"Written $filename with unsharp mask: r=$SETTING{'unsharp radius'}, s=$SETTING{'unsharp sigma'}, a=$SETTING{'unsharp amount'}, t=$SETTING{'unsharp threshold'}"
      );

      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);
      $logger->warn($e) if "$e";

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

      # Crop the image
      $e = $image->Crop( width => $w, height => $h, x => $x, y => $y );
      $logger->warn($e) if "$e";

      # Write it
      my $suffix;
      $suffix = $1 if ( $filename =~ /\.(\w*)$/ );
      ( undef, $filename ) =
        tempfile( DIR => $SETTING{session}, SUFFIX => '.' . $suffix );
      $logger->info("Cropping $w x $h + $x + $y to $filename");
      $e = $image->Write( filename => $filename );
      $logger->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;
}

# Run a user-defined tool on the selected images

sub user_defined_tool {
 my ($action) = @_;
 my @pages = $slist->get_selected_indices;

 # Update undo/redo buffers
 take_snapshot();

 my $dialog = Gtk2::Dialog->new( $d->get('Running') . "...",
  $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.
 my $pid;
 my $running   = FALSE;
 my $cancelled = FALSE;
 my $i         = 0;
 $dialog->signal_connect(
  response => sub {
   $_[0]->destroy;
   local $SIG{HUP} = 'IGNORE';
   $logger->info("Sending HUP signal to PID $pid and its children");
   killfam 'HUP', ($pid);
   $running   = FALSE;
   $cancelled = TRUE;
  }
 );
 $dialog->show_all;

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

   # To prevent any further pages being processed
   if ($cancelled) {
    return FALSE;    # uninstall
   }
   elsif ( not $running ) {
    $running = TRUE;
    $slist->get_selection->unselect_all;
    $slist->select( $pages[$i] );
    $pbar->set_text(
     sprintf( $d->get("Processing page %i of %i"), $i + 1, $#pages + 1 ) );
    $pbar->set_fraction( $i / ( $#pages + 1 ) );
    my $ifilename = $slist->{data}[ $pages[$i] ][2];
    my $format;
    $format = $1 if ( $ifilename =~ /\.(\w*)$/ );
    my ( undef, $ofilename ) =
      tempfile( DIR => $SETTING{session}, SUFFIX => ".$format" );

    my $cmd = $action->get('label');
    if ( $cmd =~ s/%o/$ofilename/g ) {
     $cmd =~ s/%i/$ifilename/g;
    }
    else {
     copy( $ifilename, $ofilename )
       or show_message_dialog( $window, 'error', 'close',
      $d->get('Error copying page') );
     $cmd =~ s/%i/$ofilename/g;
    }
    $cmd =~ s/%r/$slist->{data}[$pages[$i]][4]/g;
    $logger->info($cmd);

    $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
    $logger->info("Forked PID $pid");

    # Read without blocking
    my $output = '';
    Glib::IO->add_watch(
     fileno($read),
     [ 'in', 'hup' ],
     sub {
      my ( $fileno, $condition ) = @_;

      if ( $condition & 'hup' ) {    # bit field operation. >= would also work
       close $read;
       $logger->info('Waiting to reap process');
       my $pid = waitpid( -1, &WNOHANG );
       $logger->info("Reaped PID $pid");
       $slist->{data}[ $pages[$i] ][2] = $ofilename;

       # Update thumbnail
       $slist->{data}[ $pages[$i] ][1] =
         get_pixbuf( $ofilename, $heightt, $widtht );

       if ( $i == $#pages ) {
        $slist->select(@pages);
        $dialog->destroy;
        $cancelled = TRUE;
       }
       else {
        $slist->select( $pages[$i] );
       }
       $running = FALSE;
       $i++;
       return FALSE;    # uninstall
      }
      return TRUE;      # continue without uninstalling
     }
    );
   }
   return TRUE;         # continue without uninstalling
  }
 );
 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';
   $logger->info("Sending HUP signal to PID $pid and its children");
   killfam '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 ( not $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] );
      $logger->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$/ );
     $logger->info($cmd);

     # Interface to unpaper
     $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
     $logger->info("Forked PID $pid");
     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 $running_unpaper ) {
        close $read;
        $logger->info('Waiting to reap process');
        my $pid = waitpid( -1, &WNOHANG );
        $logger->info("Reaped PID $pid");
        return FALSE;                # uninstall
       }

       if ( ( $condition & 'hup' ) and ( not defined($line) or $line eq '' ) )
       {                             # bit field operation. >= would also work
        $logger->debug($page_buffer);
        close $read;
        $logger->info('Waiting to reap process');
        my $pid = waitpid( -1, &WNOHANG );
        $logger->info("Reaped PID $pid");

        # 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 $ofilename  = $out2;
         my $page       = $slist->{data}[$pagenum][0];
         my $resolution = $slist->{data}[$pagenum][4];
         ( my $filename, $resolution ) =
           prepare_import( $ofilename, 'Portable anymap', $resolution, TRUE );
         my $index = add_image( $filename, $page, $resolution ) + 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 ( not $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 (
   sort {
    $hashref->{$option}{options}{$a}{order} <=> $hashref->{$option}{options}{$b}
      {order}
   } 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};
   }
  }
  elsif ( $hashref->{$option}{type} eq 'SpinButtonGroup' ) {
   my @items;
   foreach ( keys %{ $hashref->{$option}{options} } ) {
    push @items, $hashref->{$option}{options}{$_}{widget}->get_value;
   }
   if (@items) {
    $SETTING{$option} = join ',', @items;
   }
  }
 }
 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 count_active_children {
 my ($frame) = @_;
 my $n = 0;
 for ( $frame->get_child->get_children ) {
  $n++ if ( $_->get_active );
 }
 return $n;
}

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 {
    $button->set_active(TRUE) if ( count_active_children($dframe) == 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' );

 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 {
    if ( count_active_children($bframe) == 0 ) {
     $bmframe->set_sensitive(FALSE);
    }
    else {
     $bmframe->set_sensitive(TRUE);
    }
   }
  );
 }
 if ( count_active_children($bframe) == 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() );

   $windowu->hide;
  }
 );

 # 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';
   $logger->info("Sending HUP signal to PID $pid and its children");
   killfam '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 ( $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;";
      }
     }
     elsif ( $SETTING{'ocr engine'} eq 'tesseract' ) {
      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'} 2> $SETTING{session}/error.txt$post";
      }
      else {
       $cmd = "$pre tesseract $tif $txt 2> $SETTING{session}/error.txt$post";
      }
     }
     elsif ( $SETTING{'ocr engine'} eq 'ocropus' ) {
      my ( $png, $pre, $post );
      if ( $slist->{data}[$pagenum][2] !~ /\.(png|jpg|pnm)$/ ) {
       my $file = $slist->{data}[$pagenum][2];

       # Temporary filename for new file
       ( undef, $png ) = tempfile( DIR => $SETTING{session}, SUFFIX => '.png' );
       $pre  = "convert $file $png;";
       $post = "; rm $png";

      }
      else {
       $png  = $slist->{data}[$pagenum][2];
       $pre  = '';
       $post = '';
      }
      if ( defined $SETTING{'ocr language'} ) {
       $cmd =
"$pre tesslanguage=$SETTING{'ocr language'} ocroscript $SETTING{ocroscript} $png > $txt.txt$post";
      }
      else {
       $cmd = "$pre ocroscript $SETTING{ocroscript} $png > $txt.txt$post";
      }
     }
     else {    # cuneiform
      my ( $bmp, $pre, $post );
      if ( $slist->{data}[$pagenum][2] !~ /\.bmp$/ ) {
       my $file = $slist->{data}[$pagenum][2];

       # Temporary filename for new file
       ( undef, $bmp ) = tempfile( DIR => $SETTING{session}, SUFFIX => '.bmp' );
       $pre  = "convert $file $bmp;";
       $post = "; rm $bmp";
      }
      else {
       $bmp  = $slist->{data}[$pagenum][2];
       $pre  = '';
       $post = '';
      }
      $cmd =
"$pre cuneiform -l $SETTING{'ocr language'} -f hocr -o $txt.txt $bmp $post";
     }
     $logger->info($cmd);

     # Interface to ocr engine
     $pid = open my $read, '-|', $cmd or die "can't open pipe: $!";
     $logger->info("Forked PID $pid");

     # 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;
        $logger->info('Waiting to reap process');
        my $pid = waitpid( -1, &WNOHANG );    # So we don't leave zombies
        $logger->info("Reaped PID $pid");
        $running_ocr = FALSE;
        if ( -r "$SETTING{session}/error.txt" ) {
         my $error = slurp("$SETTING{session}/error.txt");
         if ( $error =~ /Unable to load unicharset file/ ) {
          show_message_dialog( $windowo, 'error', 'close', $error );
          return FALSE;                       # uninstall
         }
         unlink "$SETTING{session}/error.txt";
        }

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

        # Set up the canvas
        my $canvas_new = Goo::Canvas->new;
        my $root       = $canvas_new->get_root_item;
        my $pixbuf =
          Gtk2::Gdk::Pixbuf->new_from_file( $slist->{data}[$pagenum][2] );
        $canvas_new->set_bounds( 0, 0, $pixbuf->get_width,
         $pixbuf->get_height );

        # Attach the text to the canvas
        my $output = slurp("$txt.txt");
        if ( $output =~ /<body>([\s\S]*)<\/body>/ ) {
         require HTML::TokeParser;
         require HTML::Entities;
         require utf8;
         my $p = HTML::TokeParser->new( \$output );
         my ( $x1, $y1, $x2, $y2, $text );
         while ( my $token = $p->get_token ) {
          if ( $token->[0] eq 'S' ) {
           if ( $token->[1] eq 'span'
            and defined( $token->[2]{class} )
            and $token->[2]{class} eq 'ocr_line'
            and defined( $token->[2]{title} )
            and $token->[2]{title} =~ /bbox (\d+) (\d+) (\d+) (\d+)/ )
           {
            ( $x1, $y1, $x2, $y2 ) = ( $1, $2, $3, $4 );
           }
           else {
            undef $x1;
            undef $text;
           }
          }
          if ( $token->[0] eq 'T' and $token->[1] !~ /^\s*$/ ) {
           $text = HTML::Entities::decode_entities( $token->[1] );
           utf8::encode($text);    # UTF-8 in perl sucks!
           chomp($text);
          }
          if ( $token->[0] eq 'E' ) {
           undef $x1;
           undef $text;
          }
          boxed_text( $root, $text, $x1, $y1, $x2, $y2, abs( $y2 - $y1 ) )
            if ( defined($x1) and defined($text) );
         }
        }
        else {
         boxed_text( $root, $output, 0, 0, $pixbuf->get_width,
          $pixbuf->get_height, -1 );

        }
        $slist->{data}[$pagenum][3] = $canvas_new;

        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 ) {
         $canvas = $canvas_new;
         $scwin_buffer->add($canvas);
         $canvas->set_scale( $view->get_zoom );
         $canvas->show;
        }
        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 tesseract languages

sub add_tess_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('German (Fraktur)'),
  eng     => $d->get('English'),
  fra     => $d->get('French'),
  ita     => $d->get('Italian'),
  nld     => $d->get('Dutch'),
  por     => $d->get('Portuguese'),
  slk     => $d->get('Slovak'),
  spa     => $d->get('Spanish'),
  vie     => $d->get('Vietnamese'),
 );

 my $tessdata = `tesseract '' '' -l '' 2>&1`;
 chomp $tessdata;
 $tessdata =~ s/^Unable to load unicharset file //;
 $tessdata =~ s/\/\.unicharset$//;
 $logger->info("Using tessdata at $tessdata");
 my @tesslang;
 for ( glob "$tessdata/*.unicharset" ) {

  # Weed out the empty language files
  if ( not -z $_ ) {
   my $code;
   $code = $1 if ( $_ =~ /([\w\-]*)\.unicharset$/ );
   $logger->info("Found tesseract language $code");
   if ( defined $iso639{$code} ) {
    push @tesslang, [ $code, $iso639{$code} ];
   }
   else {
    push @tesslang, [ $code, $code ];
   }
  }
 }

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

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

# Add hbox for cuneiform languages

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

 # cuneiform language codes
 my %lang = (
  eng    => $d->get('English'),
  ger    => $d->get('German'),
  fra    => $d->get('French'),
  rus    => $d->get('Russian'),
  swe    => $d->get('Swedish'),
  spa    => $d->get('Spanish'),
  ita    => $d->get('Italian'),
  ruseng => $d->get('Russian+English'),
  ukr    => $d->get('Ukrainian'),
  srp    => $d->get('Serbian'),
  hrv    => $d->get('Croatian'),
  pol    => $d->get('Polish'),
  dan    => $d->get('Danish'),
  por    => $d->get('Portuguese'),
  dut    => $d->get('Dutch'),
  cze    => $d->get('Czech'),
  rum    => $d->get('Romanian'),
  hun    => $d->get('Hungarian'),
  bul    => $d->get('Bulgarian'),
  slo    => $d->get('Slovak'),
  lav    => $d->get('Latvian'),
  lit    => $d->get('Lithuanian'),
  est    => $d->get('Estonian'),
  tur    => $d->get('Turkish'),
 );

 # Dig out supported languages
 my $cmd = "cuneiform -l";
 $logger->info($cmd);
 my $output = `$cmd`;
 $logger->info($output);

 my $langs;
 $langs = $1 if ( $output =~ /Supported languages: (.*)\./ );
 my @lang;
 for ( split " ", $langs ) {

  # Weed out the empty language files
  if ( not -z $_ ) {
   if ( defined $lang{$_} ) {
    push @lang, [ $_, $lang{$_} ];
   }
   else {
    push @lang, [ $_, $_ ];
   }
  }
 }

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

# 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 ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );

 if ( $dependencies{tesseract} ) {
  ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vbox);
  $combobe->signal_connect(
   changed => sub {
    if ($ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
    {
     $hboxtl->show_all;
    }
    else {
     $hboxtl->hide_all;
    }
   }
  );
 }
 if ( $dependencies{cuneiform} ) {
  ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vbox);
  $combobe->signal_connect(
   changed => sub {
    if ( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) {
     $hboxcl->show_all;
    }
    else {
     $hboxcl->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[ $comboboxtl->get_active ]->[0]
     if ( $SETTING{'ocr engine'} eq 'tesseract'
    or $SETTING{'ocr engine'} eq 'ocropus' );
   $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
     if ( $SETTING{'ocr engine'} eq 'cuneiform' );

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

 # 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;
 $hboxtl->hide_all
   if (
  defined($hboxtl)
  and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
   or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
   );
 $hboxcl->hide_all
   if ( defined($hboxcl)
  and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) );
 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;

 # delete $SETTING{'RMB'};

 # Write config file
 $conf->save_file( $rc, \%SETTING );
 $logger->info("Wrote config to $ENV{'HOME'}/.$prog_name");

 kill_threads();
 kill_subs();
 $logger->debug("Quitting");
 return TRUE;
}

# kill all threads

sub kill_threads {
 Gscan2pdf::Backend::Sane->kill();
 return;
}

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

 # 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 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] }{resolution} = $slist->{data}[$i][4];

  $session{ $slist->{data}[$i][0] }{buffer} = ();
  if ( defined $slist->{data}[$i][3] ) {
   my $canvas = $slist->{data}[$i][3];
   my @bounds = $canvas->get_bounds;
   push @{ $session{ $slist->{data}[$i][0] }{buffer} }, [@bounds];

   # Walk the canvas and store the text boxes as an array of hashes
   my $root = $canvas->get_root_item;
   my $n    = $root->get_n_children;
   for ( my $j = 0 ; $j < $n ; $j++ ) {
    my $group = $root->get_child($j);
    if ( $group->isa('Goo::Canvas::Group') ) {
     my $bounds = $group->get_bounds;
     my ( $x1, $y1, $x2, $y2 ) =
       ( $bounds->x1 + 1, $bounds->y1 + 1, $bounds->x2 - 1, $bounds->y2 - 1 );
     my $n = $group->get_n_children;
     for ( my $k = 0 ; $k < $n ; $k++ ) {
      my $item = $group->get_child($k);
      if ( $item->isa('Goo::Canvas::Text') ) {
       my %box;
       ( $box{x1}, $box{y1}, $box{x2}, $box{y2} ) = ( $x1, $y1, $x2, $y2 );
       $box{text}   = $item->get('text');
       $box{height} = $item->get('height');
       push @{ $session{ $slist->{data}[$i][0] }{buffer} }, {%box};
      }
     }
    }
   }
  }
 }
 my @selection = $slist->get_selected_indices;
 @{ $session{selection} } = @selection;
 store( \%session, "$SETTING{session}/session" );
 if ( defined $filename ) {
  my $tar          = Archive::Tar->new;
  my @filenamelist = glob("*");
  $logger->info( Dumper( \@filenamelist ) );
  $tar->add_files(@filenamelist);
  $tar->write( $filename, TRUE, '' );
 }
 return;
}

# Put things back to last snapshot after updating redo buffer

sub undo {
 $logger->info("Undoing");

 # Deep copy the tied data. Otherwise, very bad things happen.
 @redo_buffer = map { [@$_] } @{ $slist->{data} };
 @redo_selection = $slist->get_selected_indices;
 $logger->debug("redo_selection, undo_selection:");
 $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
 $logger->debug("redo_buffer, undo_buffer:");
 $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );

 # 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
 update_uimanager();
 $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 {
 $logger->info("Redoing");

 # Deep copy the tied data. Otherwise, very bad things happen.
 @undo_buffer = map { [@$_] } @{ $slist->{data} };
 @undo_selection = $slist->get_selected_indices;
 $logger->debug("redo_selection, undo_selection:");
 $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
 $logger->debug("redo_buffer, undo_buffer:");
 $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );

 # 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
 update_uimanager();
 $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 ($@) {
  $logger->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($?) ) {
  $logger->info("Process $pid exited.");
  delete $helperTag{$pid};
 }
 else {
  $logger->info("False alarm on $pid.");
 }
 $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:
  $logger->info("Forked PID $pid");
  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 {

 foreach ( keys %helperTag ) {
  killfam 'TERM', ($_);
  $logger->info("Sending TERM signal to PID $_ and its children");
 }
 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 );
    $logger->info("Deleting $_");
    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;
}

# Expand tildes in the filename

sub expand_tildes {
 my ($filename) = @_;
 $filename =~ s{ ^ ~ ( [^/]* ) } {
  $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($>))[7] )
 }ex;
 return $filename;
}

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

 # Default filename
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Default filename for PDFs') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $fileentry = Gtk2::Entry->new;
 $tooltips->set_tip(
  $fileentry,
  $d->get(
"\%a\t author\n\%t\t title\n\%y\t document's year\n\%Y\t today's year\n\%m\t document's month\n\%M\t today's month\n\%d\t document's day\n\%D\t today's day"
  )
 );
 $hbox->add($fileentry);
 $fileentry->set_text( $SETTING{'default filename'} )
   if defined( $SETTING{'default filename'} );

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

 # User-defined tools
 my $frame = Gtk2::Frame->new( $d->get('User-defined tools') );
 $vbox->pack_start( $frame, TRUE, TRUE, 0 );
 my $vboxt = Gtk2::VBox->new;
 $vboxt->set_border_width($border_width);
 $frame->add($vboxt);
 for my $tool (@user_defined_tools) {
  add_user_defined_tool_entry( $vboxt, $tool );
 }
 my $abutton = Gtk2::Button->new_from_stock('gtk-add');
 $vboxt->pack_start( $abutton, TRUE, TRUE, 0 );
 $abutton->signal_connect(
  clicked => sub {
   my %tool = ( cmd => 'my-tool %i %o' );
   add_user_defined_tool_menu( \%tool );
   add_user_defined_tool_entry( $vboxt, \%tool );
   $vboxt->reorder_child( $abutton, -1 );
   $SETTING{user_defined_tools} = list_user_defined_tools($vboxt);
  }
 );

 # 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{'default filename'} = $fileentry->get_text;
   $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 = expand_tildes( $tmpentry->get_text );

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

# Return list of user-defined tools
sub list_user_defined_tools {
 my ($vbox) = @_;
 my @list;
 for my $hbox ( $vbox->get_children ) {
  if ( $hbox->isa('Gtk2::HBox') ) {
   for my $widget ( $hbox->get_children ) {
    push @list, $widget->get_text if ( $widget->isa('Gtk2::Entry') );
   }
  }
 }
 return \@list;
}

# Add user-defined tool menu item
sub add_user_defined_tool_menu {
 my ($tool) = @_;
 my $action_group = Gtk2::ActionGroup->new("actions");
 $tool->{name} = "$action_group";
 my $ui = "<ui>
 <menubar name='MenuBar'>
  <menu action='Tools'>
   <menu action='User-defined'>
    <menuitem action='$tool->{name}'/>
   </menu>
  </menu>
 </menubar>
</ui>";

 # add the basic XML description of the GUI
 $tool->{merge_id} = $uimanager->add_ui_from_string($ui);

 my @action_items = (

  # Fields for each action item:
  # [name, stock_id, value, label, accelerator, tooltip, callback]
  [ $tool->{name}, undef, $tool->{cmd}, undef, undef, \&user_defined_tool ],
 );
 $action_group->add_actions( \@action_items, undef );

 # Add the actiongroup to the uimanager
 $uimanager->insert_action_group( $action_group, 0 );
 $tool->{action_group} = $action_group;
 return;
}

# Add user-defined tool entry
sub add_user_defined_tool_entry {
 my ( $vbox, $tool ) = @_;
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 my $entry = Gtk2::Entry->new;
 $entry->set_text( $tool->{cmd} );
 $entry->signal_connect(
  changed => sub {
   my $action = $tool->{action_group}->get_action( $tool->{name} );
   $action->set( 'label', $entry->get_text );
   $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
   ()    # this callback must return either 2 or 0 items.
  }
 );

 $tooltips->set_tip(
  $entry,
  $d->get(
"Use \%i and \%o for the input and output filenames respectively, or a single \%i if the image is to be modified in-place.\n\nThe other variable available is:\n\n\%r resolution"
  )
 );
 $hbox->pack_start( $entry, TRUE, TRUE, 0 );
 my $button = Gtk2::Button->new;
 $button->set_image( Gtk2::Image->new_from_stock( 'gtk-delete', 'button' ) );
 $button->signal_connect(
  clicked => sub {
   $uimanager->remove_ui( $tool->{merge_id} );
   $hbox->destroy;
   $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
  }
 );
 $hbox->pack_end( $button, FALSE, FALSE, 0 );
 $hbox->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;
}

package Gscan2pdf::Backend::Sane;

use strict;
use warnings;

use threads;
use threads::shared;
use Thread::Queue;

use Glib qw(TRUE FALSE);
use Gtk2;
use Sane;

my $_POLL_INTERVAL;
my $_self;

sub setup {
 $_POLL_INTERVAL = 100;    # ms
 $_self          = {};

 $_self->{requests} = Thread::Queue->new;
 share $_self->{device_list};
 share $_self->{device_name};

 # $_self->{device_handle} explicitly not shared
 share $_self->{status};
 share $_self->{abort_scan};
 share $_self->{scan_progress};

 $_self->{thread} = threads->new( \&_thread_main, $_self );
}

sub _enqueue_request {
 my ( $action, $data ) = @_;
 my $sentinel : shared = 0;
 $_self->{requests}->enqueue(
  {
   action   => $action,
   sentinel => \$sentinel,
   ( $data ? %{$data} : () )
  }
 );
 return \$sentinel;
}

sub _when_ready {
 my ( $sentinel, $ready_callback, $not_ready_callback ) = @_;
 Glib::Timeout->add(
  $_POLL_INTERVAL,
  sub {
   if ($$sentinel) {
    $ready_callback->();
    return Glib::SOURCE_REMOVE;
   }
   else {
    if ( defined $not_ready_callback ) {
     $not_ready_callback->();
    }
    return Glib::SOURCE_CONTINUE;
   }
  }
 );
}

sub kill {
 _enqueue_request('quit');
 $_self->{thread}->join();
 $_self->{thread} = undef;
 return;
}

sub get_devices {
 my ( $class, $container, $callback ) = @_;

 my $sentinel = _enqueue_request('get-devices');

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

 _when_ready(
  $sentinel,
  sub {
   $callback->( $_self->{device_list} );
   $pbar->destroy;
  },
  sub {
   $pbar->pulse;
  }
 );
}

sub is_connected {
 return defined $_self->{device_name};
}

sub device {
 return $_self->{device_name};
}

sub open_device {
 my ( $class, $device, $parent, $response_callback, $success_callback ) = @_;

 my $sentinel = _enqueue_request( 'open', { device_name => $device->{name} } );

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

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

 _when_ready(
  $sentinel,
  sub {
   $dialog->destroy;
   if ( $_self->{status} == SANE_STATUS_GOOD ) {
    $success_callback->();
   }
   else {
    $parent->destroy;
    main::show_message_dialog( $window, 'error', 'close',
     $d->get( 'Error opening device: ' . Sane::strstatus( $_self->{status} ) )
    );
   }
  },
  sub {
   $pbar->pulse;
  }
 );
}

sub find_scan_options {
 my ( $class, $parent, $response_callback, $success_callback ) = @_;

 my $options : shared;
 my $sentinel = _enqueue_request( 'get-options', { options => \$options } );

 # Set up ProgressBar
 my $dialog = Gtk2::Dialog->new( $d->get('Updating options') . "...",
  $parent, '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;
   $response_callback->();
  }
 );
 $dialog->show_all;

 _when_ready(
  $sentinel,
  sub {
   $dialog->destroy;
   if ( $_self->{status} == SANE_STATUS_GOOD ) {
    $success_callback->($options);
   }
   else {
    $parent->destroy;
    main::show_message_dialog(
     $window, 'error', 'close',
     $d->get(
      'Error retrieving scanner options: ' . Sane::strstatus( $_self->{status} )
     )
    );
   }
  },
  sub {
   $pbar->pulse;
  }
 );
}

sub set_option {
 my ( $class, $parent, $i, $val, $response_callback, $success_callback ) = @_;

 my $options : shared;
 my $sentinel = _enqueue_request(
  'set-option',
  {
   index       => $i,
   value       => $val,
   new_options => \$options
  }
 );

 # Set up ProgressBar
 my $dialog = Gtk2::Dialog->new( $d->get('Updating options') . "...",
  $parent, '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;
   $response_callback->();
  }
 );
 $dialog->show_all;

 _when_ready(
  $sentinel,
  sub {
   $success_callback->($options);
   $dialog->destroy;
  },
  sub {
   $pbar->pulse;
  }
 );
}

sub _new_page {
 my ( $format, $n ) = @_;
 my $path = sprintf $format, $n;
 return _enqueue_request( 'scan-page',
  { path => File::Spec->catdir( $SETTING{session}, $path ) } );
}

sub scan_pages {
 my ( $class, $parent, $format, $npages, $n, $step, $page_good_callback,
  $error_callback )
   = @_;

 $_self->{status}        = SANE_STATUS_GOOD;
 $_self->{abort_scan}    = 0;
 $_self->{scan_progress} = 0;
 my $sentinel = _new_page( $format, $n );

 my $n2      = 1;
 my $npages2 = $npages;

 # Set up ProgressBar
 my $dialog = Gtk2::Dialog->new( $d->get('Scanning...'),
  $parent, '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;          # FIXME
   $_self->{abort_scan} = 1;
  }
 );
 $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 );
 }

 Glib::Timeout->add(
  $_POLL_INTERVAL,
  sub {
   if ( !$$sentinel ) {
    $pbar->set_fraction( $_self->{scan_progress} )
      if ( defined $_self->{scan_progress} );
    return Glib::SOURCE_CONTINUE;
   }
   else {

    # Check status of scan
    if ($_self->{status} == SANE_STATUS_GOOD
     or $_self->{status} == SANE_STATUS_EOF )
    {
     $_self->{status} = SANE_STATUS_GOOD;
     $page_good_callback->($n);
    }

    # Stop the process unless everything OK and more scans required
    unless ( ( $npages == -1 or --$npages )
     and $_self->{status} == SANE_STATUS_GOOD )
    {
     _enqueue_request('cancel');
     $dialog->destroy;
     $error_callback->( Sane::strstatus( $_self->{status} ) )
       unless (
      $_self->{status} == SANE_STATUS_GOOD
      or ( $_self->{status} == SANE_STATUS_NO_DOCS
       and $npages < 1
       and $n2 > 1 )
       );
     return Glib::SOURCE_REMOVE;
    }

    $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 );
    }
    $sentinel = _new_page( $format, $n );
    return Glib::SOURCE_CONTINUE;
   }
  }
 );
}

sub _thread_main {
 my ($self) = @_;

 while ( my $request = $self->{requests}->dequeue ) {
  if ( $request->{action} eq 'quit' ) {
   last;
  }

  elsif ( $request->{action} eq 'get-devices' ) {
   _thread_get_devices($self);
  }

  elsif ( $request->{action} eq 'open' ) {
   _thread_open_device( $self, $request->{device_name} );
  }

  elsif ( $request->{action} eq 'get-options' ) {
   _thread_get_options( $self, $request->{options} );
  }

  elsif ( $request->{action} eq 'set-option' ) {
   _thread_set_option( $self, $request->{index}, $request->{value},
    $request->{new_options} );
  }

  elsif ( $request->{action} eq 'scan-page' ) {
   _thread_scan_page( $self, $request->{path} );
  }

  elsif ( $request->{action} eq 'cancel' ) {
   _thread_cancel($self);
  }

  else {
   $logger->info( "Ignoring unknown request " . $request->{action} );
   next;
  }

  # Store the current status in the shared status variable.  Otherwise, the
  # main thread has no way to access this thread's $Sane::STATUS.  Numerify to
  # please thread::shared.
  $self->{status} = $Sane::STATUS + 0;

  # Signal the sentinel that the request was completed.
  ${ $request->{sentinel} }++;
 }
}

sub _thread_get_devices {
 my ($self) = @_;
 my @devices = Sane->get_devices;
 $self->{device_list} = shared_clone \@devices;
}

sub _thread_open_device {
 my ( $self, $device_name ) = @_;

 # close the handle
 undef $self->{device_handle} if ( defined( $self->{device_handle} ) );

 $self->{device_handle} = Sane::Device->open($device_name);
 if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
  $logger->error("opening device: $Sane::STATUS");
  return;
 }
 else {
  $self->{device_name} = $device_name;
 }
}

sub _thread_get_options {
 my ( $self, $options ) = @_;
 my @options;

 # We got a device, find out how many options it has:
 my $num_dev_options = $self->{device_handle}->get_option(0);
 if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
  $logger->error("unable to determine option count");
  return;
 }
 for ( my $i = 1 ; $i < $num_dev_options ; ++$i ) {
  my $opt = $self->{device_handle}->get_option_descriptor($i);
  $options[$i] = $opt;
  $opt->{val} = $self->{device_handle}->get_option($i)
    if (
   not(( $opt->{cap} & SANE_CAP_INACTIVE )
    or ( $opt->{type} == SANE_TYPE_BUTTON )
    or ( $opt->{type} == SANE_TYPE_GROUP ) )
    );
 }

 $$options = shared_clone \@options;
}

sub _thread_set_option {
 my ( $self, $index, $value, $new_options ) = @_;

 # FIXME: Stringification to force this SV to have a PV slot.  This seems to
 # be necessary to get through Sane.pm's value checks.
 $value = "$value";

 my $info = $self->{device_handle}->set_option( $index, $value );
 $logger->info("sane_set_option returned status $Sane::STATUS with info $info");

 # FIXME: This duplicates _thread_get_options.
 if ( $info & SANE_INFO_RELOAD_OPTIONS ) {
  my $num_dev_options = $self->{device_handle}->get_option(0);
  if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
   $logger->error("unable to determine option count");
   return;
  }

  my @options;
  for ( my $i = 1 ; $i < $num_dev_options ; ++$i ) {
   my $opt = $self->{device_handle}->get_option_descriptor($i);
   $options[$i] = $opt;
   next if ( !( $opt->{cap} & SANE_CAP_SOFT_DETECT ) );

   $opt->{val} = $self->{device_handle}->get_option($i)
     if ( $opt->{type} != SANE_TYPE_BUTTON );
  }

  $$new_options = shared_clone \@options;
 }
}

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

sub _thread_scan_page_to_fh {
 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 ) {
     $logger->info("$prog_name: sane_start: $Sane::STATUS");
     goto cleanup;
    }
   }

   $parm = $device->get_parameters;
   if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
    $logger->info("$prog_name: sane_get_parameters: $Sane::STATUS");
    goto cleanup;
   }

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

    $logger->info(
     sprintf "$prog_name: acquiring %s frame",
     $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 {
      _thread_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) {
    $device->cancel if ( $_self->{abort_scan} );
    my ( $buffer, $len ) = $device->read($buffer_size);
    $total_bytes += $len;
    my $progr = $total_bytes / $hundred_percent;
    $progr = 1 if ( $progr > 1 );
    $_self->{scan_progress} = $progr;

    if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
     $logger->info( sprintf "$prog_name: min/max graylevel value = %d/%d",
      $min, $max )
       if ( $parm->{depth} == 8 );
     if ( $Sane::STATUS != SANE_STATUS_EOF ) {
      $logger->info("$prog_name: sane_read: $Sane::STATUS");
      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 );
  }
  _thread_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 ( $total_bytes > $expected_bytes && $expected_bytes != 0 ) {
  $logger->info(
   sprintf "%s: WARNING: read more data than announced by backend " . "(%u/%u)",
   $prog_name, $total_bytes, $expected_bytes );
 }
 else {
  $logger->info( sprintf "%s: read %u bytes in total",
   $prog_name, $total_bytes );
 }
 return;
}

sub _thread_scan_page {
 my ( $self, $path ) = @_;

 $self->{device_handle}->start;
 $self->{status} = $Sane::STATUS + 0;
 if ( $Sane::STATUS != SANE_STATUS_GOOD ) {
  $logger->info("$prog_name: sane_start: $Sane::STATUS");
  return;
 }

 my $fh;
 if ( not open( $fh, ">", $path ) ) {
  $self->{device_handle}->cancel;
  $self->{status} = SANE_STATUS_ACCESS_DENIED;
  return;
 }

 _thread_scan_page_to_fh( $self->{device_handle}, $fh );
 $self->{status} = $Sane::STATUS + 0;

 close $fh;

 $logger->info( sprintf "Scanned page %s. (scanner status = %d)",
  $path, $Sane::STATUS );

 if ($Sane::STATUS != SANE_STATUS_GOOD
  && $Sane::STATUS != SANE_STATUS_EOF )
 {
  unlink($path);
 }

 return;
}

sub _thread_cancel {
 my ($self) = @_;
 $self->{device_handle}->cancel;
}

__END__

=head1 Name

gscan2pdf - A GUI to produce PDFs or DjVus from scanned documents

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

gscan2pdf has the following command-line options:

=over

=item --device=<device>
Specifies the device to use, instead of getting the list of devices from via the SANE API.
This can be useful if the scanner is on a remote computer which is not broadcasting its existence.

=item --help
Displays this help page and exits.

=item --log=<log file>
Specifies a file to store logging messages.

=item --(debug|info|warn|error|fatal)
Defines the log level. If a log file is specified, this defaults to 'debug', otherwise 'warn'.

=item --version
Displays the program version and exits.

=back

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/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>. Note also that the library names given below
are the Debian/Ubuntu ones. Those distributions using RPM typically use
perl(module) where Debian has libmodule-perl.

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

a drop-in replacement for Perl threads using fork()

=item libset-intspan-perl

manages sets of integers

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

=item ocropus

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

=item cuneiform

A command line OCR. See L<http://launchpad.net/cuneiform-linux>

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

=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, but can also be used to generate the
filename see preferences for details.

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

Prints the selected or all pages.

=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

The select menus can be used to select, all, even, odd, blank, dark or modified
pages. Selecting blank or dark pages runs imagemagick to make the decision.
Selecting modified pages selects those which have modified by threshold,
unsharp, etc., since the last OCR run was made.

=head3 Preferences

The preferences menu item allows the control of the default behviour of various
functions. Most of these are self-explanatory.

=head4 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) were added.

=head4 Default filename for PDF files

The following variables are available, which are replaced by the corresponding
metadata:

 %a	author
 %t	title
 %y	document's year
 %Y	today's year
 %m	document's month
 %M	today's month
 %d	document's day
 %D	today's day

=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, tesseract, ocropus or cuneiform utilities are used to produce text from
an image.

There is an OCR output buffer for each page and is embedded as
plain text behind the scanned image in the PDF
produced. This way, Beagle can index (i.e. search) the plain text.

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://web.archive.org/web/20080529012847/http://groundstate.ca/ocr>.
An important conclusion was that 400ppi is necessary for decent results.

Up to v2.04, the only way to tell which languages were available to tesseract
was to look for the language files. Therefore, gscan2pdf checks the path
returned by:

 tesseract '' '' -l ''

If there are no language files in the above location, then gscan2pdf
assumes that tesseract v1.0 is installed, which had no language files.

=head3 Variables for user-defined tools

The following variables are available:

 %i	input filename
 %o	output filename
 %r	resolution

An image can be modified in-place by just specifying %i.


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

 Xsane
 http://scantailor.sourceforge.net/

=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
