#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process
# Copyright (C) 2006--2012 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
#    check TMPDIR regularly and display error if not enough space for another, say 2 of the largest pages.
#      To do this, keep file size in page hash.
#    use libfiu to inject file system faults into test suite
#    look at http://giv.sourceforge.net/giv/ as an alternative to make OCR layer easier
#    add support for hugin to stitch images together
#    add OCR check mode as in http://jimgarrison.org/moz-hocr-edit/hocr-edit-0.4-screenshot.png
#    Support for OCRE http://lem.eui.upm.es/ocre.html
#    add option for number of background processes
#     grep -c '^processor' /proc/cpuinfo
#     use Linux::Cpuinfo;
#     my $cpuinfo = Linux::Cpuinfo->new();
#     $cnt  = $cpuinfo->num_cpus();
#    Test coverage http://search.cpan.org/~pjcj/Devel-Cover-0.65/
#    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.
#    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
#    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
#    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"
#    Use  $tree_view->window->set_cursor( Gtk2::Gdk::Cursor->new('watch') );
#      and    $tree_view->window->set_cursor (undef); when working.
#    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
#      make tidy
#      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 (https://translations.launchpad.net/gscan2pdf)
#    Upload .pot
#    Update translators in credits (https://launchpad.net/gscan2pdf/+topcontributors)
#    Check a locale with LC_ALL=de_DE LC_MESSAGES=de_DE LC_CTYPE=de_DE LANG=de_DE LANGUAGE=de_DE bin/gscan2pdf --log=log --locale=<wherever the locale directory is>
#    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/projects/gscan2pdf/files/gscan2pdf
#    make file_releases
# 6. Freshmeat freshmeat.net/projects/gscan2pdf/releases/new (requires summary of changes)
# 7. Launchpad (https://launchpad.net/gscan2pdf/trunk/+addrelease)
# 8. 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 Gscan2pdf::Document;
use Gscan2pdf::Frontend::Sane;
use Gscan2pdf::Scanner::Options;
use Gscan2pdf::Tesseract;
use Gscan2pdf::Ocropus;
use Gscan2pdf::Cuneiform;
use Gscan2pdf::Unpaper;
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;            # To create temporary files
use Glib qw(TRUE FALSE);   # To get TRUE and FALSE
use Config::General 2.40;
use Text::ParseWords;
use Sane 0.05;             # To get SANE_NAME_PAGE_WIDTH & SANE_NAME_PAGE_HEIGHT
use PDF::API2;
use Getopt::Long;
use Set::IntSpan 1.10;     # For size method for page numbering issues
use Proc::Killfam;
use Fcntl qw(:flock)
  ;    # import LOCK_* constants to prevent us clobbering running instances
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

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

my $tolerance = 1;

# Window parameters
my $border_width = 6;

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

# Set up domain for gettext (internationalisation)
# Expects /usr/share/locale/LANGUAGE/LC_MESSAGES/$prog_name.mo
# or whatever is set by $d->dir([NEWDIR]);
our $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 );
our $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;
our %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("Perl version $^V");
$logger->info("Glib-Perl version $Glib::VERSION");
$logger->info( 'Built for Glib ' . join( '.', Glib->GET_VERSION_INFO ) );
$logger->info( 'Running with Glib '
   . join( '.', Glib::major_version, Glib::minor_version, Glib::micro_version )
);
$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");
$logger->info( 'Using Sane version ' . join( '.', Sane->get_version ) );
$logger->info("Using libsane-perl version $Sane::VERSION");

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 thread handler
Gscan2pdf->setup( $d, $logger );

# Initialise SANE frontend
Gscan2pdf::Frontend::Sane->setup( $prog_name, $d, $logger );

# Create icons for rotate buttons
my $IconFactory = undef;
my $path;
if ( -d '/usr/share/gscan2pdf' ) {
 $path = '/usr/share/gscan2pdf';
}
else {
 $path = '.';    # FIXME: 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 (
 $slist, $windowi, $windowe, $windows, $windows2, $windowh, $windowo, $windowrn,
 $windowu, $vboxd, $labeld, $combobd, $combobp, $vboxm, @undo_buffer,
 @redo_buffer, @undo_selection, @redo_selection, %dependencies,
 %helperTag,
 $scanning,   @ocr_engine,   $bscanall,       $bscannum, @clipboard, $windowd,
 $windowr,    $view,         $frames,         $duplex,   $windowp,
 $batch_scan, $gui_updating, $print_settings, $windowc,  $current_page,

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

 # Spinbuttons for selector on crop dialog
 $sb_selector_x, $sb_selector_y, $sb_selector_w, $sb_selector_h,

 # dir below session dir
 $tmpdir,

 # session dir
 $session,

 # filehandle for session lockfile
 $lockfh,
);

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-open',
  $d->get('_Open'),              '<control>o',
  $d->get('Open image file(s)'), \&open_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'), \&analyse_select_blank
 ],
 [
  'Select Dark',                'gtk-select-blank',
  $d->get('_Dark'),             '<control>d',
  $d->get('Select dark pages'), \&analyse_select_dark
 ],
 [
  'Select Modified',
  'gtk-select-modified',
  $d->get('_Modified'),
  '<control>m',
  $d->get('Select modified pages since last OCR'),
  \&select_modified_since_ocr
 ],
 [
  'Select No OCR',                            undef,
  $d->get('_No OCR'),                         undef,
  $d->get('Select pages with no OCR output'), \&select_no_ocr
 ],
 [
  'Clear OCR',                                     'gtk-clear',
  $d->get('_Clear OCR'),                           undef,
  $d->get('Clear OCR output from selected pages'), \&clear_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( 90, [ indices2pages( $slist->get_selected_indices ) ] ) }
 ],
 [
  'Rotate 180',
  'rotate180',
  $d->get('Rotate 180'),
  '<control><shift>F',
  $d->get('Rotate 180'),
  sub { rotate( 180, [ indices2pages( $slist->get_selected_indices ) ] ) }
 ],
 [
  'Rotate 270',
  'rotate270',
  $d->get('Rotate 90 anticlockwise'),
  '<control><shift>C',
  $d->get('Rotate 90 anticlockwise'),
  sub { rotate( 270, [ indices2pages( $slist->get_selected_indices ) ] ) }
 ],

 # 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='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'/>
    <menuitem action='Select No OCR'/>
   </menu>
   <menuitem action='Clear OCR'/>
   <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='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='Clear OCR'/>
  <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
our $widtht  = 100;
our $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');

# Set up a SimpleList
$slist = Gscan2pdf::Document->new;

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]->{filename} =~ /(\.\w*)$/ );
   my $new = File::Temp->new( DIR => $SETTING{session}, SUFFIX => $suffix );

   # quotes required to prevent File::Temp objects from being clobbered
   copy( "$info[2]->{filename}", "$new" )
     or show_message_dialog( $window, 'error', 'close',
    $d->get('Error copying page') );

   $info[2]->{filename} = $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
  $slist->manual_sort_by_column(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 );
 }
);

# Callback if the selection changes
$selector->{selection_changed_signal} = $selector->signal_connect(
 'selection-changed' => sub {
  my $sel = $selector->get_selection;
  if ( defined $sel ) {
   $SETTING{selection} = [ $sel->values ];
   if ( defined $sb_selector_x ) {
    $sb_selector_x->set_value( $SETTING{selection}[0] );
    $sb_selector_y->set_value( $SETTING{selection}[1] );
    $sb_selector_w->set_value( $SETTING{selection}[2] );
    $sb_selector_h->set_value( $SETTING{selection}[3] );
   }
  }
 }
);

# 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;
   $current_page = $slist->{data}[ $page[0] ][2];
   display_image($current_page);
   $selector->set_selection($sel) if ( defined $sel );
  }
  else {
   $view->set_pixbuf(undef);
   undef $current_page;
  }

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

my $unpaper = Gscan2pdf::Unpaper->new( $SETTING{'unpaper options'} );

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

update_uimanager();

# Look for crashed sessions
if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
 $tmpdir = $SETTING{TMPDIR};
}
else {
 $tmpdir = File::Spec->tmpdir;
}
$logger->info("Checking $tmpdir for crashed sessions");
my ( @sessions, @crashed, $selected ) =
  glob File::Spec->catfile( $tmpdir, 'gscan2pdf-????' );

# Forget those used by running sessions
for (@sessions) {
 if (
  open $lockfh,
  '>',
  File::Spec->catfile( $_, 'lockfile' ) and flock( $lockfh, LOCK_EX | LOCK_NB )
   )
 {
  push @crashed, $_;
  flock( $lockfh, LOCK_UN );
  close $lockfh;
 }
}

# Allow user to pick a crashed session to restore
if (@crashed) {
 my $dialog = Gtk2::Dialog->new(
  $d->get('Pick crashed session to restore'),
  $window, 'modal',
  'gtk-ok'     => 'ok',
  'gtk-cancel' => 'cancel'
 );
 my $label = Gtk2::Label->new( $d->get('Pick crashed session to restore') );
 $dialog->vbox->add($label);
 my $slist = Gtk2::Ex::Simple::List->new( $d->get('Session') => 'text', );
 push @{ $slist->{data} }, @crashed;
 $dialog->vbox->add($slist);
 $dialog->show_all;
 ($selected) = $slist->get_selected_indices if ( $dialog->run eq 'ok' );
 $dialog->destroy;

 if ( defined $selected ) {
  $session = $crashed[$selected];
  open $lockfh, '>', File::Spec->catfile( $session, 'lockfile' )    ## no critic
    or die "Cannot open lockfile";
  flock( $lockfh, LOCK_EX ) or die "Cannot lock file";
  open_session();
 }
}

# Create temporary directory if necessary
unless ( defined $session ) {
 if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
  mkdir( $SETTING{TMPDIR} ) if ( not -d $SETTING{TMPDIR} );
  unless (
   eval {
    $session = File::Temp->newdir( 'gscan2pdf-XXXX', DIR => $SETTING{TMPDIR} );
   }
    )
  {
   $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
   $Gscan2pdf::_self->{dir} = $session;
   $logger->warn(
    sprintf(
     $d->get(
"Warning: unable to use %s for temporary storage. Defaulting to %s instead."
     ),
     $SETTING{TMPDIR},
     dirname($session)
    )
   );
  }
 }
 else {
  $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
  $Gscan2pdf::_self->{dir} = "$session";
 }
 open $lockfh, '>', File::Spec->catfile( $session, 'lockfile' )    ## no critic
   or die "Cannot open lockfile";
 flock( $lockfh, LOCK_EX ) or die "Cannot lock file";
 $slist->save_session($session);
 $logger->info("Using $session for temporary files");
}

$window->show_all;

# Progress bars below window
my $phbox = Gtk2::HBox->new;
$vbox->pack_end( $phbox, FALSE, FALSE, 0 );
$phbox->show;
my $shbox = Gtk2::HBox->new;
$phbox->add($shbox);
my $spbar = Gtk2::ProgressBar->new;
$shbox->add($spbar);
my $scbutton = Gtk2::Button->new;
$scbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
$shbox->pack_end( $scbutton, FALSE, FALSE, 0 );
my $thbox = Gtk2::HBox->new;
$phbox->add($thbox);
my $tpbar = Gtk2::ProgressBar->new;
$thbox->add($tpbar);
my $tcbutton = Gtk2::Button->new;
$tcbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
$thbox->pack_end( $tcbutton, FALSE, FALSE, 0 );

### 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}   = Gscan2pdf::Tesseract->setup;
 $dependencies{ocropus}     = Gscan2pdf::Ocropus->setup;
 $dependencies{cuneiform}   = Gscan2pdf::Cuneiform->setup;
 $dependencies{djvu}        = check_command('cjb2');
 $dependencies{unpaper}     = check_command('unpaper');
 $dependencies{libtiff}     = check_command('tiffcp');
 $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 ($page) = @_;

 # quotes required to prevent File::Temp object being clobbered
 my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$page->{filename}");
 $view->set_pixbuf($pixbuf);

 # Get image dimensions to constrain selector spinbuttons on crop dialog
 ( $page->{w}, $page->{h} ) = ( $pixbuf->get_width, $pixbuf->get_height );

 # Update the ranges on the crop dialog
 if ( defined $sb_selector_w ) {
  $sb_selector_w->set_range( 0,
   $current_page->{w} - $sb_selector_x->get_value );
  $sb_selector_h->set_range( 0,
   $current_page->{h} - $sb_selector_y->get_value );
  $sb_selector_x->set_range( 0,
   $current_page->{w} - $sb_selector_w->get_value );
  $sb_selector_y->set_range( 0,
   $current_page->{h} - $sb_selector_h->get_value );

  $SETTING{selection}[0] = $sb_selector_x->get_value;
  $SETTING{selection}[1] = $sb_selector_y->get_value;
  $SETTING{selection}[2] = $sb_selector_w->get_value;
  $SETTING{selection}[3] = $sb_selector_h->get_value;

  $selector->set_selection(
   Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
 }

 # Convert hocr output to Goo:Canvas if defined
 create_canvas($page) if ( defined $page->{hocr} );
 return;
}

sub create_canvas {
 my ($page) = @_;
 $current_page = $page;

 # Set up the canvas
 $canvas = Goo::Canvas->new;
 my $root = $canvas->get_root_item;
 unless ( defined $page->{w} ) {

  # quotes required to prevent File::Temp object being clobbered
  my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$page->{filename}");
  $page->{w} = $pixbuf->get_width;
  $page->{h} = $pixbuf->get_height;
 }
 $canvas->set_bounds( 0, 0, $page->{w}, $page->{h} );

 # Attach the text to the canvas
 for my $box ( $page->boxes ) {
  my ( $x1, $y1, $x2, $y2, $text ) = @$box;
  boxed_text( $canvas, $text, $x1, $y1, $x2, $y2, abs( $y2 - $y1 ) );
 }

 # Remove the existing contents
 $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );

 $scwin_buffer->add($canvas);
 $canvas->set_scale( $view->get_zoom );
 $canvas->show;
 return;
}

# Deletes all scans after warning.

sub new {

 # Check that all pages have been saved
 for ( my $i = 0 ; $i < @{ $slist->{data} } ; $i++ ) {
  if ( not $slist->{data}[$i][2]{saved} ) {
   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;
}

# 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;
 for my $extension (@file_extensions) {
  my @filter_pattern;

  # Create case insensitive pattern
  for my $byte ( split //, $extension ) {
   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;
}

sub open_session {
 my ($filename) = @_;
 $logger->info("Restoring session in $session");
 $slist->open_session( $session, $filename );
 update_uimanager();
 return;
}

# Helper function to set up thread progress bar

sub setup_tpbar {
 my ( $thread, $process, $completed, $total, $pid ) = @_;
 if ( $total and defined($process) ) {
  $tpbar->set_text(
   sprintf(
    $d->get("Process %i of %i (%s)"), $completed + 1, $total, $process
   )
  );
  $tpbar->set_fraction( ( $completed + 0.5 ) / $total );
  $thbox->show_all;

# Pass the signal back to:
# 1. be able to cancel it when the process has finished
# 2. flag that the progress bar has been set up
#    and avoid the race condition where the callback is entered before the $completed and $total variables have caught up
  return $tcbutton->signal_connect(
   clicked => sub {
    $slist->{cancelled} = TRUE;
    $thbox->hide;
   }
  );
 }
 return;
}

# Helper function to update thread progress bar

sub update_tpbar {
 my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
 if ($total) {
  if ( defined $process ) {
   $process .= " - $message" if ( defined $message );
   $tpbar->set_text(
    sprintf(
     $d->get("Process %i of %i (%s)"),
     $completed + 1,
     $total, $process
    )
   );
  }
  else {
   $tpbar->set_text(
    sprintf( $d->get("Process %i of %i"), $completed + 1, $total ) );
  }
  if ( defined $progress ) {
   $tpbar->set_fraction( ( $completed + $progress ) / $total );
  }
  else {
   $tpbar->set_fraction( ( $completed + 0.5 ) / $total );
  }
  $thbox->show_all;
  return TRUE;
 }
 return;
}

# Throw up file selector and open selected file

sub open_dialog {

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

 my $file_chooser = Gtk2::FileChooserDialog->new(
  $d->get('Open image'),
  $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', 'gs2p' );

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

  # cd back to tempdir to import
  chdir $session;

  # Update undo/redo buffers
  take_snapshot();

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

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

  for (@filename) {
   my $signal;
   $slist->get_file_info(
    $_,
    sub {    # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {    # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $info, $pending ) = @_;
     if ( $info->{format} eq 'session file' ) {
      open_session( $info->{path} );
     }
     elsif ( $info->{pages} > 1 ) {
      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, $info->{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, $info->{pages}, 1 );
      $spinbuttonl->set_value( $info->{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 {
        $windowq->destroy;
        my $signal;
        $slist->import_file(
         $info,
         $spinbuttonf->get_value,
         $spinbuttonl->get_value,
         sub {    # queued
          my ( $process, $completed, $total, $message, $progress ) = @_;
          return update_tpbar( 1, $process, $completed, $total, $message,
           $progress );
         },
         sub {    # started
          my ( $thread, $process, $completed, $total, $pid ) = @_;
          $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
          return TRUE if ( defined $signal );
         },
         sub {    # running
          my ( $thread, $process, $completed, $total, $message, $progress ) =
            @_;
          return update_tpbar( $thread, $process, $completed, $total, $message,
           $progress );
         },
         sub {    # finished
          my ( $new_page, $pending ) = @_;
          $thbox->hide_all unless $pending;
          $tcbutton->signal_handler_disconnect($signal) if ($signal);
          update_uimanager();
         },
         sub { error_opening_image() }    # error
        );
       }
      );

      # 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;
        return;
       }
      );
      $windowq->show_all;
     }
     else {
      my $signal;
      $slist->import_file(
       $info, 1, 1,
       sub {    # queued
        my ( $process, $completed, $total, $message, $progress ) = @_;
        return update_tpbar( 1, $process, $completed, $total, $message,
         $progress );
       },
       sub {    # started
        my ( $thread, $process, $completed, $total, $pid ) = @_;
        $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
        return TRUE if ( defined $signal );
       },
       undef,    # running
       sub {     # finished
        my ( $new_page, $pending ) = @_;
        $thbox->hide_all unless $pending;
        $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
        update_uimanager();
       },
       sub { error_opening_image() }    # error
      );
     }
    },
    sub { error_opening_image() },      # error
   );
  }
 }
 else {
  $file_chooser->destroy;
 }

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

sub error_opening_image {
 show_message_dialog( $window, 'error', 'close',
  $d->get('Error opening image') );
 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]{filename};
  my $i = 1;
  while ( $i < @{ $slist->{data} } ) {
   $pagelist = $pagelist . " " . $slist->{data}[$i][2]{filename};
   ++$i;
  }
 }
 elsif ( $SETTING{'Page range'} eq 'selected' ) {
  my @page = $slist->get_selected_indices;
  $n        = $#page;
  $pagelist = $slist->{data}[ $page[0] ][2]{filename};
  my $i = 1;
  while ( $i < @page ) {
   $pagelist = $pagelist . " " . $slist->{data}[ $page[$i] ][2]{filename};
   ++$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_metadata {
 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_metadata {
 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;
}

# Draw text on the canvas with a box around it

sub boxed_text {
 my ( $canvas, $char, $x1, $y1, $x2, $y2, $size ) = @_;
 my $root = $canvas->get_root_item;
 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(
    $main::d->get('Editing text') . "...", $main::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 );
    canvas2hocr($canvas);
   }
   $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;
}

# Convert the canvas into hocr

sub canvas2hocr {
 my ($canvas) = @_;
 my ( $x, $y, $w, $h ) = $canvas->get_bounds;

 $current_page->{hocr} = <<EOS;
<!DOCTYPE html
 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
 <meta content="ocr_line ocr_page" name="ocr-capabilities"/>
 <meta content="en" name="ocr-langs"/>
 <meta content="Latn" name="ocr-scripts"/>
 <meta content="" name="ocr-microformats"/>
 <title>OCR Output</title>
</head>
<body>
 <div class="ocr_page" title="bbox $x $y $w $h>
 <p class="ocr_par">
EOS

 my $root = $canvas->get_root_item;
 for my $i ( 0 .. $root->get_n_children - 1 ) {
  my $group  = $root->get_child($i);
  my $bounds = $group->get_bounds;
  my ( $x1, $y1, $x2, $y2 ) =
    ( $bounds->x1 + 1, $bounds->y1 + 1, $bounds->x2 - 1, $bounds->y2 - 1 );

#  my $matrix = $group->get_transform; FIXME: use this as soon as Goo::Canvas 0.07 is in generally available
#  my ( $x1, $y1 ) = ( $matrix->x0, $matrix->y0 );
#  my ( $x2, $y2, $text );
  my ($text);
  for my $i ( 0 .. $group->get_n_children - 1 ) {
   my $item = $group->get_child($i);
   if ( $item->isa('Goo::Canvas::Rect') ) {

  #    ( $x2, $y2 ) = ( $x1 + $item->get('width'), $y1 + $item->get('height') );
   }
   elsif ( $item->isa('Goo::Canvas::Text') ) {
    $text = $item->get('text');
   }
   $current_page->{hocr} .=
     "  <span class=\"ocr_line\" title=\"bbox $x1 $y1 $x2 $y2\">$text</span>\n"
     if ( defined($x2) and defined($text) );
  }
 }
 $current_page->{hocr} .= "</p></div></body></html>";
 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 ( not -w dirname($filename) or ( -f $filename and not -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);

    # Compile list of pages
    my @list_of_pages;
    my @pagelist = get_page_index();
    for (@pagelist) {
     push @list_of_pages, $slist->{data}[$_][2];
    }

    # Compile options
    my %options = (
     compression      => $SETTING{'pdf compression'},
     downsample       => $SETTING{downsample},
     'downsample dpi' => $SETTING{'downsample dpi'},
     quality          => $SETTING{quality},
     font             => $SETTING{'pdf font'},
    );

    # Create the PDF
    $logger->debug("Started saving $filename");
    my $signal;
    $slist->save_pdf(
     $filename,
     \@list_of_pages,
     get_pdf_metadata(),
     \%options,
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     sub {    # running
      my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( $thread, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
      mark_pages(@pagelist);
      system("xdg-open \"$filename\" &")
        if ( defined( $SETTING{'view files toggle'} )
       and $SETTING{'view files toggle'} );
      $logger->debug("Finished saving $filename");
     },
     sub {    # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     }
    );

    $windowi->hide if defined $windowi;
   }
   $file_chooser->destroy;
  }
 );
 $file_chooser->show;

 # cd back to tempdir
 chdir $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_metadata($vboxp);

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

 # Font for non-ASCII text
 my $hboxf = Gtk2::HBox->new;
 $vbox->pack_start( $hboxf, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Font for non-ASCII text') );
 $hboxf->pack_start( $label, FALSE, FALSE, 0 );
 my @fonts;
 for (`fc-list : family style file`) {
  if (/ttf: /) {
   my ( $file, $family, $style ) = split ':';
   chomp $style;
   $family =~ s/^ //;
   $style  =~ s/^style=//;
   $style  =~ s/,.*$//;
   my $font = "$family $style";
   push @fonts, [ $file, $font, $font ];
   $SETTING{'pdf font'} = $file
     if ( not defined( $SETTING{'pdf font'} )
    and $font eq 'Times New Roman Regular' );
  }
 }
 @fonts = sort { $a->[1] cmp $b->[1] } @fonts;
 my $combof = combobox_from_array(@fonts);
 combobox_set_active( $combof, $SETTING{'pdf font'}, @fonts );
 $hboxf->pack_start( $combof, FALSE, FALSE, 0 );

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

    $SETTING{'pdf font'} = $fonts[ $combof->get_active ][0];

    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 ( not -w dirname($filename) or ( -f $filename and not -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);

       $windowi->hide if defined $windowi;
       save_TIFF($filename);
      }
      $file_chooser->destroy;
     }
    );
    $file_chooser->show;

    # cd back to tempdir
    chdir $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 ( not -w dirname($filename) or ( -f $filename and not -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);

       # Compile list of pages
       my @list_of_pages;
       my @pagelist = get_page_index();
       for (@pagelist) {
        push @list_of_pages, $slist->{data}[$_][2];
       }

       my $signal;
       $slist->save_text(
        $filename,
        \@list_of_pages,
        sub {    # queued
         my ( $process, $completed, $total, $message, $progress ) = @_;
         return update_tpbar( 1, $process, $completed, $total, $message,
          $progress );
        },
        sub {    # started
         my ( $thread, $process, $completed, $total, $pid ) = @_;
         $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
         return TRUE if ( defined $signal );
        },
        sub {    # running
         my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
         return update_tpbar( $thread, $process, $completed, $total, $message,
          $progress );
        },
        sub {    # finished
         my ( $new_page, $pending ) = @_;
         $thbox->hide_all unless $pending;
         $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
         mark_pages(@pagelist);
         system("xdg-open \"$filename\" &")
           if ( defined( $SETTING{'view files toggle'} )
          and $SETTING{'view files toggle'} );
         $logger->debug("Finished saving $filename");
        },
        sub {    # error
         show_message_dialog( $main::window, 'error', 'close',
          $Gscan2pdf::_self->{message} );
        }
       );
       $windowi->hide if defined $windowi;
      }
      $file_chooser->destroy;
     }
    );
    $file_chooser->show;

    # cd back to tempdir
    chdir $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 ( not -w dirname($filename) or ( -f $filename and not -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 $tif = File::Temp->new( DIR => $session, SUFFIX => '.tif' );
       $windowi->hide if defined $windowi;
       save_TIFF( $tif->filename, $filename );
      }
      $file_chooser->destroy;
     }
    );
    $file_chooser->show;

    # cd back to tempdir
    chdir $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 ( not -w dirname($filename) or ( -f $filename and not -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);
       $slist->save_session( $session, $filename );
      }
      $file_chooser->destroy;
     }
    );
    $file_chooser->show;

    # cd back to tempdir
    chdir $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' );

 if ( $type[ $combobi->get_active ][0] ne 'pdf' ) {
  $vboxp->hide_all;
  $hboxf->hide_all;
 }
 $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 ( not -w dirname($filename) or ( -f $filename and not -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 $session;

  # Compile list of pages
  my @list_of_pages;
  my @pagelist = get_page_index();
  for (@pagelist) {
   push @list_of_pages, $slist->{data}[$_][2];
  }

  if ( @list_of_pages > 1 ) {
   my $w = length( scalar @list_of_pages );
   for ( my $i = 1 ; $i <= @list_of_pages ; $i++ ) {
    my $current_filename = sprintf "${filename}_%0${w}d.$SETTING{'image type'}",
      $i;
    return if ( file_exists( $file_chooser, $current_filename ) );
   }
   $filename = "${filename}_%0${w}d.$SETTING{'image type'}";
  }
  else {
   $filename = $filename . ".$SETTING{'image type'}"
     if ( $filename !~ /\.$SETTING{'image type'}$/i );
   return if ( file_exists( $file_chooser, $filename ) );
  }

  # Create the image
  $logger->debug("Started saving $filename");
  my $signal;
  $slist->save_image(
   $filename,
   \@list_of_pages,
   sub {    # queued
    my ( $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( 1, $process, $completed, $total, $message, $progress );
   },
   sub {    # started
    my ( $thread, $process, $completed, $total, $pid ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   sub {    # running
    my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( $thread, $process, $completed, $total, $message,
     $progress );
   },
   sub {    # finished
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    mark_pages(@pagelist);
    system("xdg-open \"$filename\" &")
      if ( defined( $SETTING{'view files toggle'} )
     and $SETTING{'view files toggle'} );
    $logger->debug("Finished saving $filename");
   },
   sub {    # error
    show_message_dialog( $main::window, 'error', 'close',
     $Gscan2pdf::_self->{message} );
   }
  );

  $windowi->hide if defined $windowi;
 }
 $file_chooser->destroy;
 return;
}

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

 # Compile list of pages
 my @list_of_pages;
 my @pagelist = get_page_index();
 for (@pagelist) {
  push @list_of_pages, $slist->{data}[$_][2];
 }

 # Compile options
 my %options = (
  compression => $SETTING{'tiff compression'},
  quality     => $SETTING{quality},
 );

 my $signal;
 $slist->save_tiff(
  $filename,
  \@list_of_pages,
  \%options,
  $ps,
  sub {    # queued
   my ( $process, $completed, $total, $message, $progress ) = @_;
   return update_tpbar( 1, $process, $completed, $total, $message, $progress );
  },
  sub {    # started
   my ( $thread, $process, $completed, $total, $pid ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  sub {    # running
   my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
   return update_tpbar( $thread, $process, $completed, $total, $message,
    $progress );
  },
  sub {    # finished
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   mark_pages(@pagelist);
   my $file = defined($ps) ? $ps : $filename;
   system("xdg-open \"$filename\" &")
     if ( defined( $SETTING{'view files toggle'} )
    and $SETTING{'view files toggle'} );
   $logger->debug("Finished saving $filename");
  },
  sub {    # error
   show_message_dialog( $main::window, 'error', 'close',
    $Gscan2pdf::_self->{message} );
  }
 );
 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 ( not -w dirname($filename) or ( -f $filename and not -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);

    # Compile list of pages
    my @list_of_pages;
    my @pagelist = get_page_index();
    for (@pagelist) {
     push @list_of_pages, $slist->{data}[$_][2];
    }

    # cd back to tempdir
    chdir $session;

    # Create the DjVu
    $logger->debug("Started saving $filename");
    my $signal;
    $slist->save_djvu(
     $filename,
     \@list_of_pages,
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     sub {    # running
      my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( $thread, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
      mark_pages(@pagelist);
      system("xdg-open \"$filename\" &")
        if ( defined( $SETTING{'view files toggle'} )
       and $SETTING{'view files toggle'} );
      $logger->debug("Finished saving $filename");
     },
     sub {    # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     }
    );

    $windowi->hide if defined $windowi;
   }
   $file_chooser->destroy;
  }
 );
 $file_chooser->show;

 # cd back to tempdir
 chdir $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_metadata($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 );

   # Compile list of pages
   my @list_of_pages;
   my @pagelist = get_page_index();
   for (@pagelist) {
    push @list_of_pages, $slist->{data}[$_][2];
   }

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

   # Compile options
   my %options = (
    compression      => $SETTING{'pdf compression'},
    downsample       => $SETTING{downsample},
    'downsample dpi' => $SETTING{'downsample dpi'},
    quality          => $SETTING{quality},
    font             => $SETTING{'pdf font'},
   );

   my $pdf = File::Temp->new( DIR => $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
   my $signal;
   $slist->save_pdf(
    $pdf->filename,
    \@list_of_pages,
    get_pdf_metadata(),
    \%options,
    sub {    # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {    # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    sub {    # running
     my ( $thread, $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( $thread, $process, $completed, $total, $message,
      $progress );
    },
    sub {    # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     mark_pages(@pagelist);
     system("xdg-open \"$pdf\" &")
       if ( defined( $SETTING{'view files toggle'} )
      and $SETTING{'view files toggle'} );
     $logger->info($mua_string);
     show_message_dialog( $main::window, 'error', 'close',
      $main::d->get('Error creating email') )
       if ( system($mua_string) );
    },
    sub {    # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    }
   );

   $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 );
   $unpaper->add_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
     $SETTING{'unpaper options'} = $unpaper->get_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: $!";  ## no critic
 $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 );
   $unpaper->add_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
     $SETTING{'unpaper options'} = $unpaper->get_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;

 my $pbar;
 Gscan2pdf::Frontend::Sane->get_devices(
  sub {

   # Set up ProgressBar
   $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;
  },
  sub {
   $pbar->pulse;
  },
  sub {
   my ($data) = @_;
   $pbar->destroy;
   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[$index]->{name};
    scan_options( $notebook, $device_list[$index] );
   }
  }
 );
 $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 $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");

  $spbar->set_text( $d->get('Scanning') );
  $shbox->show_all;
  my $signal = $scbutton->signal_connect(
   clicked => sub {
    local $SIG{INT} = 'IGNORE';
    $logger->info("Sending INT signal to PID $pid and its children");
    killfam 'INT', ($pid);
    $cancel = TRUE;
   }
  );

  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;
       $spbar->set_fraction($fraction);
      }
      elsif ( $line =~ /^Scanning (-?\d*) pages/ ) {
       $spbar->set_text(
        $d->get('Scanning') . " $1 " . $d->get('pages') . "..." );
      }
      elsif ( $line =~ /^Scanning page (\d*)/ ) {
       $spbar->set_text(
        sprintf( $d->get('Scanning page %i...'), $1 * $step + $offset ) );
       $spbar->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/ )
      {
       $spbar->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/ )
      {
       $scbutton->signal_handler_disconnect($signal);
       $shbox->hide;
       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/ ) {
       $scbutton->signal_handler_disconnect($signal);
       $shbox->hide;
       show_message_dialog( $windows, 'error', 'close',
        $d->get('Device busy') );
      }
      elsif (
       $line =~ /^$SETTING{frontend}: sane_read: Operation was cancelled/ )
      {
       $scbutton->signal_handler_disconnect($signal);
       $shbox->hide;
       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;
     $scbutton->signal_handler_disconnect($signal);
     $shbox->hide;
     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 $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");

  $spbar->set_text( $d->get('Scanning') );
  $spbar->set_pulse_step(.1);
  $shbox->show_all;
  my $running = TRUE;
  my $signal  = $scbutton->signal_connect(
   clicked => sub {
    local $SIG{HUP} = 'IGNORE';
    $logger->info("Sending INT signal to PID $pid and its children");
    killfam 'HUP', ($pid);
    $running = FALSE;
   }
  );

  # Timer will run until callback returns false
  my $size;
  my $id = 1;
  $spbar->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) {
       $spbar->set_fraction( ( -s "out$id.pnm" ) / $size );
      }
      else {
       $spbar->pulse;
      }
     }
     elsif ( -e "out$id.pnm" and ( -s "out$id.pnm" ) > 50 ) {
      $size = get_size_from_PNM("out$id.pnm");
     }
     else {
      $spbar->pulse;
     }
     return TRUE;
    }
    else {
     return FALSE;
    }
   }
  );

  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/ )
      {
       $spbar->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 );
         $spbar->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/ ) {
       $scbutton->signal_handler_disconnect($signal);
       $shbox->hide;
       $running = FALSE;
       show_message_dialog( $windows, 'error', 'close',
        $d->get('Device busy') );
      }
      elsif (
       $line =~ /^$SETTING{frontend}: sane_read: Operation was cancelled/ )
      {
       $scbutton->signal_handler_disconnect($signal);
       $shbox->hide;
       $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;
     $scbutton->signal_handler_disconnect($signal);
     $shbox->hide;
     $running = FALSE;
     return FALSE;                         # uninstall
    }
    return TRUE;                           # continue without uninstalling
   }
  );
  $scanning = TRUE;
 }
 else {
  $logger->info($cmd);
 }
 return;
}

sub update_spbar {
 my ( $i, $npages ) = @_;
 if ( $npages > 0 ) {
  $spbar->set_text( sprintf $d->get("Scanning page %d of %d"), $i, $npages );
 }
 else {
  $spbar->set_text( sprintf $d->get("Scanning page %d"), $i );
 }
 $spbar->set_fraction(0);
 return;
}

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

 my $signal;
 Gscan2pdf::Frontend::Sane->scan_pages(
  dir              => $session,
  format           => $format,
  npages           => $npages,
  start            => $start,
  step             => $step,
  started_callback => sub {
   update_spbar( $i, $npages );
   $shbox->show_all;
   $signal = $scbutton->signal_connect(
    clicked => sub {
     $Gscan2pdf::_self->{abort_scan} = 1;
    }
   );
  },
  running_callback => sub {
   my ($progress) = @_;
   $spbar->set_fraction($progress) if ( defined $progress );
  },
  finished_callback => sub {
   $scbutton->signal_handler_disconnect($signal) if ( defined $signal );
   scan_options( $notebook, $device_list[ $combobd->get_active ] )
     if ( $SETTING{'cycle sane handle'} );
   $shbox->hide;
  },
  new_page_callback => sub {
   my ($n) = @_;
   $i++;

   # If the scan can't be loaded then blow the scanning dialog away and
   # show an error
   my $rotate = $n % 2 ? $rfacing : $rreverse;
   if ( defined $test_image ) {
    import_scan( $test_image, $n, undef, FALSE, $rotate, $unpaper, $ocr );
   }
   else {
    $SETTING{resolution} =
      defined( $SETTING{resolution} )
      ? $SETTING{resolution}
      : $Gscan2pdf::Document::POINTS_PER_INCH;
    $logger->info("Importing scan with resolution=$SETTING{resolution}");
    import_scan( File::Spec->catdir( $session, "out$n.pnm" ),
     $n, $SETTING{resolution}, TRUE, $rotate, $unpaper, $ocr );
   }

   update_spbar( $i, $npages );
  },
  error_callback => sub {
   $scbutton->signal_handler_disconnect($signal) if ( defined $signal );
   $shbox->hide;
   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 ( $pages, $rotate, $unpaper, $ocr ) = @_;

 if ($rotate) {
  rotate(
   $rotate, $pages,
   sub {
    my ($pages) = @_;
    post_process_scan( $pages, undef, $unpaper, $ocr );
   }
  );
 }
 elsif ($unpaper) {
  unpaper_page(
   $pages,
   $unpaper->get_cmdline,
   sub {
    my ($pages) = @_;
    post_process_scan( $pages, undef, undef, $ocr );
   }
  );
 }
 elsif ($ocr) {
  ocr_page($pages);
 }
 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: $!";   ## no critic

 # 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 $gpage = Gscan2pdf::Page->new(
      filename   => $ofilename,
      resolution => $resolution,
      format     => 'Portable anymap',
      delete     => $delete,
      dir        => $session,
     );
     my $index = $slist->add_page( $gpage, $page, &update_uimanager );
     if ( $index == -1 ) {
      show_message_dialog( $windows, 'error', 'close',
       $d->get('Unable to load image') );
     }
     else {
      update_uimanager();
      post_process_scan( [$gpage], $rotate, $unpaper, $ocr );
     }
     return FALSE;    # uninstall
    }
   }
   return TRUE;       # continue without uninstalling
  }
 );

 return;
}

# 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]{filename}")
     ;    # quotes required to prevent File::Temp object being clobbered
   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 $session;
 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 {

 # We only add pages if the list of thumbnails has the focus
 if ( $slist->has_focus ) {
  my @page = $slist->get_selected_indices;

  # Create a new image file for each page in the clipboard
  for (@clipboard) {
   my $suffix;
   $suffix = $1 if ( $_->[2] =~ /(\.\w*)$/ );
   my $new = File::Temp->new( DIR => $session, SUFFIX => $suffix );
   unless ( copy( $_->[2], $new ) ) {
    show_message_dialog( $window, 'error', 'close',
     $d->get('Error pasting image') );
    return;
   }
   $_->[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 a page is selected, then insert the pasted page immediately afterwards
  if (@page) {
   splice @{ $slist->{data} }, $page[0] + 1, 0, @clipboard;
   @page = ( $page[0] + 1 );
  }

  # Otherwise append the page to the end
  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][2]{dirty_time};
  my $ocr_flag   = $slist->{data}[$page][2]{ocr_flag};
  my $ocr_time   = $slist->{data}[$page][2]{ocr_time};
  $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;
}

# Select pages with no ocr output

sub select_no_ocr {
 my @selection;
 for ( 0 .. $#{ $slist->{data} } ) {
  push @selection, $_ unless ( defined $slist->{data}[$_][2]{hocr} );
 }

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

# Clear the OCR output from selected pages

sub clear_ocr {

 # Update undo/redo buffers
 take_snapshot();

 # Remove the existing canvas
 $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );

 my @selection = $slist->get_selected_indices;
 for (@selection) {
  delete $slist->{data}[$_][2]{hocr};
 }
 return;
}

# Analyse and select blank pages

sub analyse_select_blank {
 analyse( 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][2]{dirty_time};
  my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Std Dev to threshold
  if ( $slist->{data}[$page][2]{std_dev} <= $SETTING{'Blank threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting blank page");
  }
  $logger->info( "StdDev: "
     . $slist->{data}[$page][2]{std_dev}
     . " threshold: "
     . $SETTING{'Blank threshold'} );
 }
 return;
}

# Analyse and select dark pages

sub analyse_select_dark {
 analyse( 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][2]{dirty_time};
  my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Mean to threshold
  if ( $slist->{data}[$page][2]{mean} <= $SETTING{'Dark threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting dark page");
  }
 }
 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;
Torsten Schönfeld
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--2012 Jeffrey Ratcliffe') );
 my $licence = <<EOS;
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright 2006 -- 2012 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 required around EOS because of UTF-8 in $translators
Utku BERBEROĞLU
Oleg Koptev
zeugma
Daniel Dietrich
Fitoschido
Сергій Дубик
maidis
Serkan Guldal
Artem Karimov
Tayfun Kayhan
Igor Zubarev
Tico
Eugene Marshal
pp/bs
Gábor Sepsi
Úr Balázs
Szenográdi Norbert Péter
jimpap
andreas
Jessica Tallon
EOS
 $about->set_translator_credits($translators);
 $about->set_artists('lodp');
 $about->run;
 $about->destroy;
 return;
}

# Check that a command exists

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

# Rescan device-dependent scan options

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

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

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

 # Get the options from the cache if defined
 my $mode;
 if ( defined $SETTING{mode} ) {
  $mode = $SETTING{mode};
 }
 else {
  $mode = 'default';
 }
 if ( $SETTING{'cache options'} and defined $SETTING{cache}{$device}{$mode} ) {
  my $options  = options2hash( $SETTING{cache}{$device}{$mode} );
  my $response = 'cancel';
  unless ( $options->num_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: $!"; ## no critic
  $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 = Gscan2pdf::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, $options ) = @_;

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

 # 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
 for ( 0 .. $options->num_options - 1 ) {
  my $option = $options->by_index($_);
  if ( defined( $pddo{ $option->{name} } ) ) {
   my ( $widget, $hbox ) = add_widget_from_hash($option);

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

   # If an ADF isn't selected, then we don't want to scan all pages
   if ( $option->{name} eq 'source' ) {
    $widget->signal_connect(
     changed => sub {
      my $text = $widget->get_active_text;
      if ( defined($text)
       and get_value( \%ddo, $option->{name}, $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->{name}, $text ) =~
        /(ADF|Automatic Document Feeder)/i )
       {
        $batch_scan->set_active(0) if ( defined $batch_scan );
       }
      }
      if ( defined($text)
       and get_value( \%ddo, $option->{name}, $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->{name} eq 'mode' ) {
    $widget->signal_connect(
     changed => sub {
      $SETTING{ $option->{name} } =
        get_value( \%ddo, $option->{name}, $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, $options );
    }
    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, $options );
 }

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

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

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

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

 # Widget
 my $widget;

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

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

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

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

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

 $hbox->pack_end( $widget, FALSE, FALSE, 0 );
 $tooltips->set_tip( $widget, $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, $options ) = @_;

 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, $options );
   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, $options ) = @_;
 my (@ignored);
 for ( keys %{ $SETTING{Paper} } ) {
  if ( $options->supports_paper( $SETTING{Paper}{$_}, $tolerance ) ) {
   $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';
  unless ( $options->num_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: $!"; ## no critic
  $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 = Gscan2pdf::slurp($filename);
  $vboxd->hide_all;    # merely here for consistency with normal operation
  update_options_hash( $vboxd, options2hash($output) );
 }
 return;
}

# return the options, updated with the user defaults

sub options2hash {

 my ($output) = @_;
 my $options = Gscan2pdf::Scanner::Options->new($output);
 $logger->debug( Dumper($options) );
 for ( 0 .. $options->num_options - 1 ) {
  my $option = $options->by_index($_);

  # Set default from config
  if (
   defined( $SETTING{ $option->{name} } )
   and $option->{default} ne 'inactive'

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

  # in the case that we've switched devices and the new device doesn't support
  # the new mode
  elsif ( $option->{name} eq 'mode' ) {
   delete $SETTING{ $option->{name} };
  }
 }

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

# walk the widget tree and update them from the hash

sub update_options_hash {

 my ( $vboxd, $options ) = @_;
 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( $options->by_name($key)->{values} )
      )
    {

     # Empty the list
     $widget->get_model->clear;

     # Fill it again
     my $index = default2index(
      $options->by_name($key)->{default},
      @{ $options->by_name($key)->{values} }
     );
     foreach ( @{ $options->by_name($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 ( $options->by_name($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( $options->by_name($key)->{max} ) )
    {
     $widget->set_range( $options->by_name($key)->{min},
      $options->by_name($key)->{max} );

     # Set the default
     if ( $options->by_name($key)->{default} =~ /inactive/ ) {
      $hbox->set_sensitive(FALSE);
     }
     else {
      $widget->set_value( $options->by_name($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
   $options->delete_by_name($key) if ( defined $options->by_name($key) );
  }
 }

 # Add any new options
 for ( 0 .. $options->num_options - 1 ) {
  my $option = $options->by_index($_);
  if ( defined( $pddo{ $option->{name} } )
   and $option->{name} !~ /^page-?(width|height)/ )
  {
   my ( $widget, $hbox ) = add_widget_from_hash($option);
   $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 );

  my $signal;
  Gscan2pdf::Frontend::Sane->open_device(
   device_name      => $device->{name},
   started_callback => sub {
    $spbar->set_text( $d->get('Opening device') );
    $spbar->set_pulse_step(.1);
    $shbox->show_all;
    $signal = $scbutton->signal_connect(
     clicked => sub {
      kill_subs();
     }
    );
   },
   running_callback => sub {
    $spbar->pulse;
   },
   finished_callback => sub {
    $scbutton->signal_handler_disconnect($signal);
    $shbox->hide;
    Gscan2pdf::Frontend::Sane->find_scan_options(
     sub {    # started callback
      $spbar->set_text( $d->get('Opening device') );
      $spbar->set_pulse_step(.1);
      $shbox->show_all;
      $signal = $scbutton->signal_connect(
       clicked => sub {
        kill_subs();
       }
      );
     },
     sub {    # running callback
      $spbar->pulse;
     },
     sub {    # finished callback
      my ($data) = @_;
      my $options = Gscan2pdf::Scanner::Options->new($data);
      $logger->debug( "Sane->get_option_descriptor returned: ",
       Dumper($options) );

      my ( $group, $vbox, @widgets, $hboxp, %optnamei );
      my $num_dev_options = $options->num_options;

      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->by_index($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 )
         or ( $opt->{name} eq SANE_NAME_PAGE_HEIGHT )
         or ( $opt->{name} eq SANE_NAME_PAGE_WIDTH ) )
         )
       {

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

        # 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
        $options->{box}{ $opt->{name} } = $hbox
          if ( $opt->{name} eq SANE_NAME_SCAN_BR_X
         or $opt->{name} eq SANE_NAME_SCAN_BR_Y
         or $opt->{name} eq SANE_NAME_SCAN_TL_X
         or $opt->{name} eq SANE_NAME_SCAN_TL_Y
         or $opt->{name} eq SANE_NAME_PAGE_HEIGHT
         or $opt->{name} eq SANE_NAME_PAGE_WIDTH );

# Only define the paper size once the rest of the geometry widget have been created
        if (
             defined( $options->{box}{ scalar(SANE_NAME_SCAN_BR_X) } )
         and defined( $options->{box}{ scalar(SANE_NAME_SCAN_BR_Y) } )
         and defined( $options->{box}{ scalar(SANE_NAME_SCAN_TL_X) } )
         and defined( $options->{box}{ scalar(SANE_NAME_SCAN_TL_Y) } )
         and ( not defined $options->by_name(SANE_NAME_PAGE_HEIGHT)
          or defined( $options->{box}{ scalar(SANE_NAME_PAGE_HEIGHT) } ) )
         and ( not defined $options->by_name(SANE_NAME_PAGE_WIDTH)
          or defined( $options->{box}{ scalar(SANE_NAME_PAGE_WIDTH) } ) )
         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, $options );
           }
           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,
              SANE_NAME_PAGE_HEIGHT, SANE_NAME_PAGE_WIDTH
             )
              )
            {
             $options->{box}{$_}->show_all if ( defined $options->{box}{$_} );
            }
           }
           else {
            my $paper = $combobp->get_active_text;
            if ( defined( $options->by_name(SANE_NAME_PAGE_HEIGHT) )
             and defined( $options->by_name(SANE_NAME_PAGE_WIDTH) ) )
            {
             $widgets[ $options->by_name(SANE_NAME_PAGE_HEIGHT)->{index} ]
               ->set_value(
              $SETTING{Paper}{$paper}{y} + $SETTING{Paper}{$paper}{t} );
             $widgets[ $options->by_name(SANE_NAME_PAGE_WIDTH)->{index} ]
               ->set_value(
              $SETTING{Paper}{$paper}{x} + $SETTING{Paper}{$paper}{l} );
            }
            $widgets[ $options->by_name(SANE_NAME_SCAN_TL_X)->{index} ]
              ->set_value( $SETTING{Paper}{$paper}{l} );
            $widgets[ $options->by_name(SANE_NAME_SCAN_TL_Y)->{index} ]
              ->set_value( $SETTING{Paper}{$paper}{t} );
            $widgets[ $options->by_name(SANE_NAME_SCAN_BR_X)->{index} ]
              ->set_value(
             $SETTING{Paper}{$paper}{x} + $SETTING{Paper}{$paper}{l} );
            $widgets[ $options->by_name(SANE_NAME_SCAN_BR_Y)->{index} ]
              ->set_value(
             $SETTING{Paper}{$paper}{y} + $SETTING{Paper}{$paper}{t} );
            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,
                SANE_NAME_PAGE_HEIGHT, SANE_NAME_PAGE_WIDTH
               )
                )
              {
               $options->{box}{$_}->hide_all if ( defined $options->{box}{$_} );
              }
             }
            );
           }
          }
         );
         add_paper( $combobp, $options );
        }

       }
       else {
        $logger->warn("Unknown type $opt->{type}");
       }
      }

      # Set defaults
      my $sane_device = Gscan2pdf::Frontend::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->by_index( $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->by_index( $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;

      $scbutton->signal_handler_disconnect($signal);
      $shbox->hide;
     },
     sub {    # error callback
      my ($message) = @_;
      $windows2->destroy;
      main::show_message_dialog( $window, 'error', 'close',
       $d->get( 'Error retrieving scanner options: ' . $message ) );
     }
    );
   },
   error_callback => sub {
    my ($message) = @_;
    $windows2->destroy;
    main::show_message_dialog( $window, 'error', 'close',
     $d->get( 'Error opening device: ' . $message ) );
   }
  );

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

  my $signal;
  Gscan2pdf::Frontend::Sane->set_option(
   index            => $i,
   value            => $val,
   started_callback => sub {
    $spbar->set_text( $d->get('Updating options') );
    $spbar->set_pulse_step(.1);
    $shbox->show_all;
    $signal = $scbutton->signal_connect(
     clicked => sub {
      kill_subs();
     }
    );
   },
   running_callback => sub {
    $spbar->pulse;
   },
   finished_callback => 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.
    $scbutton->signal_handler_disconnect($signal);
    $shbox->hide;
   }
  );
  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;
     $slist->manual_sort_by_column(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;
}

# Helper function to convert an array of indices into an array of Gscan2pdf::Page objects

sub indices2pages {
 my @indices = @_;
 my @pages;
 for (@indices) {
  push @pages, $slist->{data}[$_][2];
 }
 return @pages;
}

# Rotate selected images

sub rotate {
 my ( $angle, $pagelist, $callback ) = @_;

 # Update undo/redo buffers
 take_snapshot();

 for my $page ( @{$pagelist} ) {
  my $signal;
  $slist->rotate(
   $angle, $page,
   sub {    # queued
    my ( $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( 1, $process, $completed, $total, $message, $progress );
   },
   sub {    # started
    my ( $thread, $process, $completed, $total, $pid ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   undef,    # running
   sub {     # finished
    my ( $new_page, $pending ) = @_;
    $callback->($new_page) if $callback;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   sub {     # error
    show_message_dialog( $main::window, 'error', 'close',
     $Gscan2pdf::_self->{message} );
   },
   sub {     # display
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }
 return;
}

# Analyse selected images

sub analyse {
 my ( $select_blank, $select_dark ) = @_;

 # Update undo/redo buffers
 take_snapshot();

 foreach my $i ( 0 .. $#{ $slist->{data} } ) {
  my $dirty_time   = $slist->{data}[$i][2]{dirty_time};
  my $analyse_time = $slist->{data}[$i][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->info(
"Updating: $slist->{data}[$i][0] analyse_time: $analyse_time dirty_time: $dirty_time"
   );
   my $signal;
   $slist->analyse(
    $slist->{data}[$i][2],
    sub {    # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {    # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     select_blank_pages()                          if $select_blank;
     select_dark_pages()                           if $select_dark;
    },
    sub {     # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    },
   );
  }
 }
 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 @pagelist = get_page_index();
   my $page     = 0;
   for my $i (@pagelist) {
    $page++;
    my $signal;
    $slist->threshold(
     $SETTING{'threshold tool'},
     $slist->{data}[$i][2],
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     undef,    # running
     sub {     # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     sub {     # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     },
     sub {     # display
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }
    }

 );

 # 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 @pagelist = get_page_index();
   for my $i (@pagelist) {
    my $signal;
    $slist->negate(
     $slist->{data}[$i][2],
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     undef,    # running
     sub {     # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     sub {     # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     },
     sub {     # display
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }

  }
 );

 # 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 @pagelist = get_page_index();
   for my $i (@pagelist) {
    my $signal;
    $slist->unsharp(
     $slist->{data}[$i][2],
     $SETTING{'unsharp radius'},
     $SETTING{'unsharp sigma'},
     $SETTING{'unsharp amount'},
     $SETTING{'unsharp threshold'},
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     undef,    # running
     sub {     # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     sub {     # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     },
     sub {     # display
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }

  }
 );

 # 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 and defined( $SETTING{selection} ) ) {
  $selector->signal_handler_block( $selector->{selection_changed_signal} );
  $selector->set_selection(
   Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
  $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
 }
 return;
}

# Display page selector and on apply crop accordingly

sub crop {

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

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

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

 # Frame for page range
 add_page_range($vbox);

 # SpinButton for x
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('x') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_x = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
 $tooltips->set_tip( $sb_selector_x,
  $d->get('The x-position of the left hand edge of the crop.') );
 $hbox->pack_end( $sb_selector_x, FALSE, TRUE, 0 );

 # SpinButton for y
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('y') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_y = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
 $tooltips->set_tip( $sb_selector_y,
  $d->get('The y-position of the top edge of the crop.') );
 $hbox->pack_end( $sb_selector_y, FALSE, TRUE, 0 );

 # SpinButton for w
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Width') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_w = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
 $tooltips->set_tip( $sb_selector_w, $d->get('The width of the crop.') );
 $hbox->pack_end( $sb_selector_w, FALSE, TRUE, 0 );

 # SpinButton for h
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Height') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_h = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
 $tooltips->set_tip( $sb_selector_h, $d->get('The height of the crop.') );
 $hbox->pack_end( $sb_selector_h, FALSE, TRUE, 0 );

 # Callbacks if the spinbuttons change
 $sb_selector_x->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[0] = $sb_selector_x->get_value;
   $sb_selector_w->set_range( 0, $current_page->{w} - $SETTING{selection}[0] );
   update_selector();
  }
 );
 $sb_selector_y->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[1] = $sb_selector_y->get_value;
   $sb_selector_h->set_range( 0, $current_page->{h} - $SETTING{selection}[1] );
   update_selector();
  }
 );
 $sb_selector_w->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[2] = $sb_selector_w->get_value;
   $sb_selector_x->set_range( 0, $current_page->{w} - $SETTING{selection}[2] );
   update_selector();
  }
 );
 $sb_selector_h->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[3] = $sb_selector_h->get_value;
   $sb_selector_y->set_range( 0, $current_page->{h} - $SETTING{selection}[3] );
   update_selector();
  }
 );

 $sb_selector_x->set_value( $SETTING{selection}[0] )
   if ( defined $SETTING{selection}[0] );
 $sb_selector_y->set_value( $SETTING{selection}[1] )
   if ( defined $SETTING{selection}[1] );
 $sb_selector_w->set_value( $SETTING{selection}[2] )
   if ( defined $SETTING{selection}[2] );
 $sb_selector_h->set_value( $SETTING{selection}[3] )
   if ( defined $SETTING{selection}[3] );

 # 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 ( not defined $sel );
   my ( $x, $y, $w, $h ) = $sel->values;

   # Update undo/redo buffers
   take_snapshot();

   my @pagelist = get_page_index();
   for my $i (@pagelist) {
    my $signal;
    $slist->crop(
     $slist->{data}[$i][2],
     $x, $y, $w, $h,
     sub {    # queued
      my ( $process, $completed, $total, $message, $progress ) = @_;
      return update_tpbar( 1, $process, $completed, $total, $message,
       $progress );
     },
     sub {    # started
      my ( $thread, $process, $completed, $total, $pid ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     undef,    # running
     sub {     # finished
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     sub {     # error
      show_message_dialog( $main::window, 'error', 'close',
       $Gscan2pdf::_self->{message} );
     },
     sub {     # display
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }

  }
 );

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

 $windowc->show_all;
 return;
}

sub update_selector {
 my $sel = $selector->get_selection;
 $selector->signal_handler_block( $selector->{selection_changed_signal} );
 $selector->set_selection(
  Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) )
   if ( defined $sel );
 $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
 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 $cmd = $action->get('label');
 for my $i (@pages) {
  my $signal;
  $slist->user_defined(
   $slist->{data}[$i][2],
   $cmd,
   sub {    # queued
    my ( $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( 1, $process, $completed, $total, $message, $progress );
   },
   sub {    # started
    my ( $thread, $process, $completed, $total, $pid ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   undef,    # running
   sub {     # finished
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   sub {     # error
    show_message_dialog( $main::window, 'error', 'close',
     $Gscan2pdf::_self->{message} );
   },
   sub {     # display
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }
 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;
}

# queue $page to be processed by unpaper

sub unpaper_page {
 my ( $pages, $options, $callback ) = @_;
 $options = '' if ( !defined($options) );

 # Update undo/redo buffers
 take_snapshot();

 for my $pageobject ( @{$pages} ) {
  my $signal;
  $slist->unpaper(
   $pageobject,
   $options,
   sub {    # queued
    my ( $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( 1, $process, $completed, $total, $message, $progress );
   },
   sub {    # started
    my ( $thread, $process, $completed, $total, $pid ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   undef,    # running
   sub {     # finished
    my ( $new_page, $pending ) = @_;
    $callback->($new_page) if $callback;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   sub {     # error
    show_message_dialog( $main::window, 'error', 'close',
     $Gscan2pdf::_self->{message} );
   },
   sub {     # display
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }

 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 );
 $unpaper->add_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
   $SETTING{'unpaper options'} = $unpaper->get_options;

   # run unpaper
   my @pages = indices2pages( get_page_index() );
   unpaper_page( \@pages, $unpaper->get_cmdline );

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

 for my $page ( @{$pages} ) {
  my $signal;
  if ( $SETTING{'ocr engine'} eq 'gocr' ) {
   $slist->gocr(
    $page,
    sub {    # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {    # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    },
    sub {     # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    },
    sub {     # display
     my ($new_page) = @_;
     my @page = $slist->get_selected_indices;
     create_canvas($new_page)
       if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
    }
   );
  }
  elsif ( $SETTING{'ocr engine'} eq 'tesseract' ) {
   $slist->tesseract(
    $page,
    $SETTING{'ocr language'},
    sub {     # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {     # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    },
    sub {     # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    },
    sub {     # display
     my ($new_page) = @_;
     my @page = $slist->get_selected_indices;
     create_canvas($new_page)
       if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
    }
   );
  }
  elsif ( $SETTING{'ocr engine'} eq 'ocropus' ) {
   $slist->ocropus(
    $page,
    $SETTING{'ocr language'},
    sub {     # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {     # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    },
    sub {     # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    },
    sub {     # display
     my ($new_page) = @_;
     my @page = $slist->get_selected_indices;
     create_canvas($new_page)
       if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
    }
   );
  }
  else {      # cuneiform
   $slist->cuneiform(
    $page,
    $SETTING{'ocr language'},
    sub {     # queued
     my ( $process, $completed, $total, $message, $progress ) = @_;
     return update_tpbar( 1, $process, $completed, $total, $message,
      $progress );
    },
    sub {     # started
     my ( $thread, $process, $completed, $total, $pid ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    undef,    # running
    sub {     # finished
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    },
    sub {     # error
     show_message_dialog( $main::window, 'error', 'close',
      $Gscan2pdf::_self->{message} );
    },
    sub {     # display
     my ($new_page) = @_;
     my @page = $slist->get_selected_indices;
     create_canvas($new_page)
       if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
    }
   );
  }
 }
 return;
}

# 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 @tesslang;
 for ( keys %{ Gscan2pdf::Tesseract->languages } ) {
  push @tesslang, [ $_, $d->get( ${ Gscan2pdf::Tesseract->languages }{$_} ) ];
 }

 # If there are no language files, then we have tesseract-1.0, i.e. English
 unless (@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 );

 # Tesseract language files
 my @lang;
 for ( keys %{ Gscan2pdf::Cuneiform->languages } ) {
  push @lang, [ $_, $d->get( ${ Gscan2pdf::Cuneiform->languages }{$_} ) ];
 }

 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 @pages = indices2pages( get_page_index() );
   if ( not @pages ) {
    show_message_dialog( $windowo, 'error', 'close',
     $d->get('No page selected') );
    return;
   }
   ocr_page( \@pages );
   $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 ( not $slist->{data}[$i][2]{saved} ) {
   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;
   }
  }
 }

 # Make sure that we are back in the start directory, otherwise we can't delete
 # the temp dir.
 chdir $SETTING{'cwd'};

 # Remove temporary files (for some reason File::Temp wasn't doing its job here)
 unlink <$session/*>;
 rmdir $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::Frontend::Sane->quit();
 Gscan2pdf->quit();
 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
 $slist->save_session($session);
 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;
}

# 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}[$_][2]{saved} = TRUE;
 }
 $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
 return;
}

# Convert all files in temp that are not jpg, png, or tiff to png,

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

 for ( @{ $slist->{data} } ) {
  my $signal;
  $slist->to_png(
   $_->[2],
   sub {    # queued
    my ( $process, $completed, $total, $message, $progress ) = @_;
    return update_tpbar( 1, $process, $completed, $total, $message, $progress );
   },
   sub {    # started
    my ( $thread, $process, $completed, $total, $pid ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   undef,    # running
   sub {     # finished
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   sub {     # error
    show_message_dialog( $main::window, 'error', 'close',
     $Gscan2pdf::_self->{message} );
   },
  );
 }
 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);
 my $cbcsh =
   Gtk2::CheckButton->new_with_label( $d->get('Cycle SANE handle after scan') );
 $combob->signal_connect(
  changed => sub {

   if ( $frontends[ $combob->get_active ][0] eq 'libsane-perl' ) {
    $cbcsh->set_sensitive(TRUE);
   }
   else {
    $cbcsh->set_sensitive(FALSE);
   }
  }
 );
 combobox_set_active( $combob, $SETTING{frontend}, @frontends );
 $hbox->pack_end( $combob, TRUE, TRUE, 0 );

 # Cycle SANE handle after scan
 $cbcsh->set_active( $SETTING{'cycle sane handle'} )
   if ( defined $SETTING{'cycle sane handle'} );
 $vbox->pack_start( $cbcsh, 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($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);
  }
 );

 # View saved files
 my $cbv = Gtk2::CheckButton->new_with_label( $d->get('View files on saving') );
 $cbv->set_active( $SETTING{'view files toggle'} )
   if ( defined $SETTING{'view files toggle'} );
 $vbox->pack_start( $cbv, TRUE, TRUE, 0 );

 # Apply button
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 $button = Gtk2::Button->new_from_stock('gtk-apply');
 $hbox->pack_start( $button, TRUE, TRUE, 0 );
 $button->signal_connect(
  clicked => sub {
   $windowr->hide;
   if ( $SETTING{frontend} ne $frontends[ $combob->get_active ][0] ) {
    $SETTING{frontend} = $frontends[ $combob->get_active ][0];
    if ( $SETTING{frontend} eq 'libsane-perl' ) {
     $windows->destroy if ( defined $windows );
    }
    else {
     $windows2->destroy if ( defined $windows2 );
     rescan_options( $vboxd, $device[ $combobd->get_active ] )
       if ( defined $windows );
    }
   }
   $SETTING{'cycle sane handle'} = $cbcsh->get_active;
   $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($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];

   # Store viewer preferences
   $SETTING{'view files toggle'} = $cbv->get_active;
  }
 );

 # 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}[$_][2]{resolution} = $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 ][2]{resolution} if ( @page > 0 );
 for (@page) {
  if ( $slist->{data}[$_][2]{resolution} != $resolution ) {
   $resolution = '';
   last;
  }
 }
 return $resolution;
}

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

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

Gtk2->main;

package Gscan2pdf::PageRange;    ## no critic

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

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

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

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

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

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

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

__END__

=head1 Name

gscan2pdf - A GUI to produce 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<http://sourceforge.net/projects/gscan2pdf/files/gscan2pdf/>).

=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 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 log file created by C<gscan2pdf --log=log> 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 Open

Opens 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
