#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process

# 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)
#    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. If necessary bump with something like
#     xargs sed -i "s/\(\$VERSION *= \)'2\.5\.3'/\1'2.5.4'/" < MANIFEST
#    Make appropriate updates to ../debian/changelog
# 2.  perl Makefile.PL
#     Upload .pot
# 3.  make remote-html
# 4.  git status
#     git tag vx.x.x
#     git push --tags ssh://ra28145@git.code.sf.net/p/gscan2pdf/code master
#    If the latter doesn't work, try:
#     git push --tags https://ra28145@git.code.sf.net/p/gscan2pdf/code master
#     make signed_tardist
# 5. Build package for debian
#     sudo DIST=sid pbuilder update
#     DIST=sid pdebuild
#    or
#     sudo sbuild-update -udr sid-amd64-sbuild
#     sbuild -sc sid-amd64-sbuild
#    debsign .changes
#    lintian -iI --pedantic .changes
#    check contents with dpkg-deb --contents
#    test dist sudo dpkg -i gscan2pdf_x.x.x_all.deb
#     dput ftp-master .changes
# 6. create version directory in https://sourceforge.net/projects/gscan2pdf/files/gscan2pdf
#     make file_releases
# 7. Build packages for Ubuntu
#    name the release -0~ppa1<release>, where release is eoan, disco, cosmic, bionic, xenial (dh9), trusty (< v2), precise (< v2), etc
#     debuild -S -sa
#     dput gscan2pdf-ppa .changes
#    https://launchpad.net/~jeffreyratcliffe/+archive
# 8. gscan2pdf-announce@lists.sourceforge.net, gscan2pdf-help@lists.sourceforge.net, gnome-announce-list@gnome.org, sane-devel@lists.alioth.debian.org

use warnings;
use strict;
use feature 'switch';
no if $] >= 5.018, warnings => 'experimental::smartmatch';

use Gscan2pdf::Dialog::MultipleMessage;
use Gscan2pdf::Dialog::Renumber;
use Gscan2pdf::Dialog::Save;
use Gscan2pdf::Dialog::Scan::CLI;
use Gscan2pdf::Dialog::Scan::Image_Sane;
use Gscan2pdf::Document;
use Gscan2pdf::Frontend::Image_Sane;
use Gscan2pdf::Frontend::CLI;
use Gscan2pdf::Scanner::Profile;
use Gscan2pdf::Tesseract;
use Gscan2pdf::Ocropus;
use Gscan2pdf::Cuneiform;
use Gscan2pdf::Unpaper;
use Gscan2pdf::Canvas;
use Gscan2pdf::Config;
use Gscan2pdf::Translation '__';    # easier to extract strings with xgettext
use Image::Sane ':all';             # To get SANE_* enums
use Image::Magick;
use Gscan2pdf::ImageView;
use Gtk3::SimpleList;

# -init should not be necessary, as we use $app->run, but without it,
# the config file is saved with the numeric locale
# and the application name is shown as Perl in GNOME 3
use Gtk3 0.028 -init;
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 File::Path qw(remove_tree);
use Glib qw(TRUE FALSE);    # To get TRUE and FALSE
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;
use Try::Tiny;
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
use Filesys::Df;
use English qw( -no_match_vars )
  ;    # for $PERL_VERSION, $PROGRAM_NAME, $EVAL_ERROR, $ERRNO

# To sort out LC_NUMERIC and $SIG{CHLD}
use POSIX qw(locale_h :signal_h :errno_h :sys_wait_h);
use Date::Calc qw(Delta_DHMS Add_Delta_DHMS Today_and_Now Timezone);
use Locale::gettext;

# to deal with utf8 in filenames
use Encode qw(_utf8_off _utf8_on);

# Bind the Gio API
# This is necessary mainly for Gtk3::Application
BEGIN {
    use Glib::Object::Introspection;
    Glib::Object::Introspection->setup(
        basename => 'Gio',
        version  => '2.0',
        package  => 'Glib::IO'
    );
}

use Readonly;
Readonly my $HALF                    => 0.5;
Readonly my $UNIT_SLIDER_STEP        => 0.001;
Readonly my $SIGMA_STEP              => 0.1;
Readonly my $MAX_SIGMA               => 5;
Readonly my $_90_DEGREES             => 90;
Readonly my $_180_DEGREES            => 180;
Readonly my $_270_DEGREES            => 270;
Readonly my $DRAGGER_TOOL            => 10;
Readonly my $SELECTOR_TOOL           => 20;
Readonly my $PAINTER_TOOL            => 30;
Readonly my $TABBED_VIEW             => 100;
Readonly my $SPLIT_VIEW              => 101;
Readonly my $EMPTY_LIST              => -1;
Readonly my $_100_PERCENT            => 100;
Readonly my $MAX_DPI                 => 2400;
Readonly my $BITS_PER_BYTE           => 8;
Readonly my $RIGHT_MOUSE_BUTTON      => 3;
Readonly my $HELP_WINDOW_WIDTH       => 800;
Readonly my $HELP_WINDOW_HEIGHT      => 600;
Readonly my $HELP_WINDOW_DIVIDER_POS => 200;
Readonly my $_1KB                    => 1024;
Readonly my $_1MB                    => $_1KB * $_1KB;
Readonly my $_100_000MB              => 100_000;

Glib::set_application_name('gscan2pdf');
my $prog_name = Glib::get_application_name;
my $VERSION   = '2.5.4';

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

my $debug    = FALSE;
my $EMPTY    = q{};
my $SPACE    = q{ };
my $DOT      = q{.};
my $PERCENT  = q{%};
my $ASTERISK = q{*};
my $d_sane   = Locale::gettext->domain('sane-backends');
my (
    $test,   $help,       $log,    $log_level, @device,
    $locale, $test_image, $logger, $import
);
parse_arguments();

# Catch and log perl warnings
local $SIG{__WARN__} = sub {
    local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1;
    $logger->warn(@_);
};

my ( $rc, %SETTING ) = read_config();

set_up_test_mode();

$logger->info("Operating system: $OSNAME");
if ( $OSNAME eq 'linux' ) {
    my @files = glob '/etc/*-release';
    for (@files) {
        my $output = Gscan2pdf::Document::slurp($_);
        if ( defined $output ) {
            chomp $output;
            $logger->info($output);
        }
    }
}
$logger->info("Perl version $PERL_VERSION");
$logger->info("Glib-Perl version $Glib::VERSION");
$logger->info(
    "Glib::Object::Introspection version $Glib::Object::Introspection::VERSION"
);
$logger->info( 'Built for Glib ' . join $DOT, Glib->GET_VERSION_INFO );
$logger->info(
    'Running with Glib ' . join $DOT, Glib::major_version,
    Glib::minor_version,              Glib::micro_version
);
$logger->info("Gtk3-Perl version $Gtk3::VERSION");
$logger->info( 'Built for GTK ' . join $DOT,
    ( Gtk3->MAJOR_VERSION, Gtk3->MINOR_VERSION, Gtk3->MICRO_VERSION ) );
$logger->info(
    'Running with GTK ' . join $DOT,
    ( Gtk3::get_major_version, Gtk3::get_minor_version,
        Gtk3::get_micro_version
    )
);
$logger->info("Gscan2pdf::Document version $Gscan2pdf::Document::VERSION");

#$logger->info( 'Using GtkImageView version ',
#    Gtk3::ImageView->library_version );
#$logger->info("Using Gtk3::ImageView version $Gtk3::ImageView::VERSION");
$logger->info("Using PDF::API2 version $PDF::API2::VERSION");
$logger->info( 'Using Sane version ' . join $DOT, Image::Sane->get_version );
$logger->info("Using libimage-sane-perl version $Image::Sane::VERSION");

if ($debug) { $logger->debug( Dumper( \%SETTING ) ) }

if ( ( not defined $SETTING{version} or $SETTING{version} ne $VERSION )
    and defined $SETTING{cache} )
{
    delete $SETTING{cache};
}
$SETTING{version} = $VERSION;

# Initialise thread handler
Gscan2pdf::Document->setup($logger);

# Initialise SANE frontends
Gscan2pdf::Frontend::Image_Sane->setup($logger);
Gscan2pdf::Frontend::CLI->setup($logger);

# Update list in Gscan2pdf::Document so that it can be used by get_resolution
Gscan2pdf::Document->set_paper_sizes( $SETTING{Paper} );

# Create icons for rotate buttons
my $iconfactory;
my $iconpath;
if ( -d '/usr/share/gscan2pdf' ) {
    $iconpath = '/usr/share/gscan2pdf';
}
else {
    $iconpath = 'icons';
}
init_icons(
    [ 'rotate90',    "$iconpath/stock-rotate-90.svg" ],
    [ 'rotate180',   "$iconpath/stock-rotate-180.svg" ],
    [ 'rotate270',   "$iconpath/stock-rotate-270.svg" ],
    [ 'scanner',     "$iconpath/scanner.svg" ],
    [ 'pdf',         "$iconpath/pdf.svg" ],
    [ 'selection',   "$iconpath/stock-selection-all-16.png" ],
    [ 'hand-tool',   "$iconpath/hand-tool.svg" ],
    [ 'mail-attach', "$iconpath/mail-attach.svg" ],
);

# Define application-wide variables here so that they can be referenced
# in the menu callbacks
my (
    $slist,     $windowi,     $windowe, $windows, $windowo, $windowrn, $windowu,
    $windowudt, $save_button, $window,  $thbox,   $tpbar,
    $tcbutton, $spbar, $shbox, $scbutton, $unpaper, $hpaned,
    @undo_buffer,
    @redo_buffer,    @undo_selection, @redo_selection, %dependencies,
    $menubar,        $toolbar,
    @ocr_engine,     $clipboard,
    $windowr,        $view,           $windowp,        $message_dialog,
    $print_settings, $windowc,        $current_page,   $gconftool,

    # start page on scan dialog
    $start,

    # Goo::Canvas for OCR output
    $canvas,

    # Notebook, split panes for detail view and OCR output
    $vnotebook, $hpanei,

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

    # Temp::File object for PDF to be emailed
    # Define here to make sure that it doesn't get deleted until the next email
    # is created or we quit
    $pdf,

    # hash of true type fonts available. Used by PDF OCR output
    $fonts,

    # SimpleList in preferences dialog
    $option_visibility_list,

    # Comboboxes for user-defined tools and rotate buttons
    $comboboxudt, $rotate_side_cmbx, $rotate_side_cmbx2,

    # Declare the XML structure
    $uimanager,
);

# Create the window
my $app = Gtk3::Application->new( 'org.gscan2pdf', 'handles-open' );
$app->signal_connect( 'startup'  => \&application_startup_callback );
$app->signal_connect( 'activate' => \&application_activate_callback );
$app->run;

sub populate_main_window {
    my $main_vbox = Gtk3::VBox->new;
    $window->add($main_vbox);

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

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

    # Scrolled window for thumbnails
    my $scwin_thumbs = Gtk3::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;

    # If dragged below the bottom of the window, scroll it.
    $slist->signal_connect( 'drag-motion' => \&drag_motion_callback );

    # Set up callback for right mouse clicks.
    $slist->signal_connect( button_press_event   => \&handle_clicks );
    $slist->signal_connect( button_release_event => \&handle_clicks );

    # Update the start spinbutton if the page number is been edited.
    $slist->get_model->signal_connect( 'row-changed' => sub { update_start() }
    );

    $scwin_thumbs->add($slist);

    # Notebook, split panes for detail view and OCR output
    $vnotebook = Gtk3::Notebook->new;
    $hpanei    = Gtk3::HPaned->new;
    $hpanei->show;

    # ImageView for detail view
    $view = Gscan2pdf::ImageView->new;

    $view->signal_connect(
        button_press_event => sub {
            my ( $widget, $event ) = @_;
            handle_clicks( $widget, $event );
        }
    );
    $view->signal_connect( button_release_event => \&handle_clicks );
    $view->{zoom_changed_signal} = $view->signal_connect(
        'zoom-changed' => sub {
            if ( defined $canvas ) {
                $canvas->signal_handler_block( $canvas->{zoom_changed_signal} );
                $canvas->set_scale( $view->get_zoom );
                $canvas->signal_handler_unblock(
                    $canvas->{zoom_changed_signal} );
            }
        }
    );
    $view->{offset_changed_signal} = $view->signal_connect(
        'offset-changed' => sub {
            if ( defined $canvas ) {
                my $offset = $view->get_offset;
                $canvas->signal_handler_block(
                    $canvas->{offset_changed_signal} );
                $canvas->set_offset( $offset->{x}, $offset->{y} );
                $canvas->signal_handler_unblock(
                    $canvas->{offset_changed_signal} );
            }
        }
    );

    # Callback if the selection changes
    $view->{selection_changed_signal} = $view->signal_connect(
        'selection-changed' => sub {
            my ( $widget, $sel ) = @_;
            if ( defined $sel ) {
                $SETTING{selection} = $sel;
                if ( defined $sb_selector_x ) {
                    $sb_selector_x->set_value( $SETTING{selection}{x} );
                    $sb_selector_y->set_value( $SETTING{selection}{y} );
                    $sb_selector_w->set_value( $SETTING{selection}{width} );
                    $sb_selector_h->set_value( $SETTING{selection}{height} );
                }
            }
        }
    );

    # Goo::Canvas for OCR output
    $canvas = Gscan2pdf::Canvas->new;
    $canvas->{zoom_changed_signal} = $canvas->signal_connect(
        'zoom-changed' => sub {
            $view->signal_handler_block( $view->{zoom_changed_signal} );
            $view->set_zoom( $canvas->get_scale );
            $view->signal_handler_unblock( $view->{zoom_changed_signal} );
        }
    );
    $canvas->{offset_changed_signal} = $canvas->signal_connect(
        'offset-changed' => sub {
            $view->signal_handler_block( $view->{offset_changed_signal} );
            my $offset = $canvas->get_offset;
            $view->set_offset( $offset->{x}, $offset->{y} );
            $view->signal_handler_unblock( $view->{offset_changed_signal} );
        }
    );
    pack_viewer_tools();

    # Set up call back for list selection to update detail view
    $slist->{selection_changed_signal} = $slist->get_selection->signal_connect(
        changed => \&selection_changed_callback );

    # _after ensures that Editables get first bite
    $window->signal_connect_after(
        key_press_event => sub {
            my ( $widget, $event ) = @_;

            # Let the keypress propagate
            if ( $event->keyval != Gtk3::Gdk::KEY_Delete ) { return FALSE }

            delete_selection();
            return TRUE;
        }
    );

    # If defined in the config file, set the current directory
    if ( not defined $SETTING{'cwd'} ) { $SETTING{'cwd'} = getcwd }

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

    update_uimanager();

    create_temp_directory();

    $window->show_all;

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

    # Open scan dialog in background
    if ( $SETTING{'auto-open-scan-dialog'} ) { scan_dialog( undef, TRUE ) }

    # Deal with --import command line option
    if ( defined $import ) { import_files($import) }
    return;
}

# Pack widgets according to viewer_tools

sub pack_viewer_tools {
    if ( $SETTING{viewer_tools} == $TABBED_VIEW ) {
        $vnotebook->append_page( $view, Gtk3::Label->new( __('Image') ) );
        $vnotebook->append_page( $canvas,
            Gtk3::Label->new( __('OCR Output') ) );
        $hpaned->pack2( $vnotebook, TRUE, TRUE );
        $vnotebook->show_all;
    }
    else {    # $SPLIT_VIEW
        $hpanei->pack1( $view, TRUE, TRUE );
        $hpanei->pack2( $canvas, TRUE, TRUE );
        $hpaned->pack2( $hpanei, TRUE, TRUE );
    }
    return;
}

### Subroutines

sub parse_arguments {
    my @args = (
        'device=s'     => \@device,
        'test=s%'      => \$test,
        'test-image=s' => \$test_image,
        'import=s'     => \$import,
        '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 },
    );
    if ( not GetOptions(@args) ) { exit 1 }
    $Image::Sane::DEBUG = $debug;

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

    if ( defined $log ) {
        $log_conf .= <<"EOS";
 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
EOS
    }
    else {
        $log_conf .= <<"EOS";
 log4perl.category                  = $log_level, Screen
EOS
    }

    if ( defined $help ) {
        system("perldoc $PROGRAM_NAME") == 0
          or die __('Error displaying help'), "\n";
    }

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

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

    if ( defined $locale ) {
        if ( $locale =~ /^\//xsm ) {
            Gscan2pdf::Translation::set_domain( $prog_name, $locale );
        }
        else {
            Gscan2pdf::Translation::set_domain( $prog_name,
                getcwd . "/$locale" );
        }
    }
    else {
        Gscan2pdf::Translation::set_domain($prog_name);
    }

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

sub read_config {

    # config files:
    # - old: $HOME/.gscan2pdf
    # - new: $XDG_CONFIG_HOME/gscan2pdfrc or $HOME/.config/gscan2pdfrc
    my $rcf      = "$ENV{'HOME'}/.${prog_name}";
    my $newrcdir = $ENV{'XDG_CONFIG_HOME'} || "$ENV{'HOME'}/.config";
    my $newrc    = "${newrcdir}/${prog_name}rc";

    my %config = Gscan2pdf::Config::read_config(
        ( -e $newrc ) ? $newrc : $rcf,    # use new config file if it exists
        $logger
    );

    # migrate from old config file location to XDG-compliant one
    if ( -d $newrcdir and not -e $newrc ) {
        Gscan2pdf::Config::write_config( $newrc, $logger, \%config );
        unlink $rcf;
    }
    $rcf = $newrc;

    Gscan2pdf::Config::add_defaults( \%config );
    Gscan2pdf::Config::remove_invalid_paper( $config{Paper} );

    # Delete the options cache if there is a new version of SANE
    Gscan2pdf::Config::check_sane_version( \%config,
        join( $DOT, Image::Sane->get_version ),
        $Image::Sane::VERSION );
    return $rcf, %config;
}

sub set_up_test_mode {

    # 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/gsm;
            $logger->debug("'$file','$device'");
            if ( $file !~ /^\//xsm ) { $file = getcwd . "/$file" }
            if ( not -r $file ) {
                $logger->fatal( sprintf __('Cannot read file: %s'), $file );
                exit 1;
            }
            push @{ $test->{file} }, $file;
            if ( not defined $test->{output} ) { $test->{output} = $EMPTY }
            $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 __('Cannot read file: %s'), $test_image );
            exit 1;
        }
    }
    return;
}

sub application_startup_callback {
    $window = Gtk3::ApplicationWindow->new($app);
    $window->set_title("$prog_name v$VERSION");
    $window->signal_connect(
        'delete-event' => sub {
            if ( quit() ) {
                $app->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} );
        if ( defined $SETTING{window_x} and defined $SETTING{window_y} ) {
            $window->move( $SETTING{window_x}, $SETTING{window_y} );
        }
        if ( $SETTING{window_maximize} ) { $window->maximize }
    }

    try { $window->set_icon_from_file("$iconpath/gscan2pdf.svg"); }
    catch {
        $logger->warn(
            "Unable to load icon `$iconpath/gscan2pdf.svg': $EVAL_ERROR");
    };

    $app->add_window($window);
    populate_main_window($window);
    return;
}

sub application_activate_callback {
    $window->present;
    return;
}

# Check for presence of various packages

sub check_dependencies {
    my $image = Image::Magick->new;
    if ( $image->Get('version') =~ /ImageMagick\s([\d.]+)/xsm ) {
        $dependencies{perlmagick} = $1;
    }
    $dependencies{tesseract} = Gscan2pdf::Tesseract->setup($logger);
    $dependencies{ocropus}   = Gscan2pdf::Ocropus->setup($logger);
    $dependencies{cuneiform} = Gscan2pdf::Cuneiform->setup($logger);
    $dependencies{unpaper}   = Gscan2pdf::Unpaper->version;
    if ( $dependencies{perlmagick} ) {
        $logger->info("Found Image::Magick $dependencies{perlmagick}");
    }
    if ( $dependencies{unpaper} ) {
        $logger->info("Found unpaper $dependencies{unpaper}");
    }
    my @dependencies = (
        [
            'imagemagick', 'stdout',
            qr/Version:\sImageMagick\s([\d.-]+)/xsm,
            [ 'convert', '--version' ]
        ],
        [
            'scanadf', 'stdout',
            qr/scanadf\s[(]sane-frontends[)]\s([\d.]+)/xsm,
            [ 'scanadf', '--version' ]
        ],
        [
            'xdg', 'stdout',
            qr/xdg-email\s([^\n]+)/xsm, [ 'xdg-email', '--version' ]
        ],
        [ 'gocr', 'stderr', qr/gocr\s([^\n]+)/xsm, [ 'gocr', '-h' ] ],
        [
            'djvu', 'stderr', qr/DjVuLibre-([\d.]+)/xsm, [ 'cjb2', '--version' ]
        ],
        [
            'libtiff', 'stderr',
            qr/LIBTIFF,\sVersion\s([\d.]+)/xsm, [ 'tiffcp', '-h' ]
        ],

        # pdftops and pdfunite are both in poppler-utils, and so the version is
        # the version is the same.
        # Both are needed, though to update %dependencies
        [
            'pdftops', 'stderr',
            qr/pdftops\sversion\s([\d.]+)/xsm, [ 'pdftops', '-v' ]
        ],
        [
            'pdfunite', 'stderr',
            qr/pdfunite\sversion\s([\d.]+)/xsm, [ 'pdfunite', '-v' ]
        ],
        [ 'pdf2ps', 'stdout', qr/([\d.]+)/xsm, [ 'gs',    '--version' ] ],
        [ 'pdftk',  'stdout', qr/([\d.]+)/xsm, [ 'pdftk', '--version' ] ],
    );
    for (@dependencies) {
        my ( $name, $stream, $regex, $cmd ) = @{$_};
        $dependencies{$name} =
          Gscan2pdf::Document::program_version( $stream, $regex, $cmd );
        if ( $dependencies{$name} and $dependencies{$name} eq '-1' ) {
            delete $dependencies{$name};
        }
        if ( $dependencies{$name} ) {
            $logger->info("Found $name $dependencies{$name}");
            if ( $name eq 'pdftk' ) {

                # Create PDF via tiff2pdf, not directly with imagemagick, as
                # some distros configure imagemagick not to write PDFs
                my $temptif =
                  File::Temp->new( DIR => $session, SUFFIX => '.tif' );
                Gscan2pdf::Document::exec_command(
                    [ 'convert', 'rose:', $temptif ] );
                my $temppdf =
                  File::Temp->new( DIR => $session, SUFFIX => '.pdf' );
                Gscan2pdf::Document::exec_command(
                    [ 'tiff2pdf', '-o', $temppdf, $temptif ] );
                ( undef, my $out ) = Gscan2pdf::Document::exec_command(
                    [ $name, $temppdf, 'dump_data' ] );
                if ( $out !~ /NumberOfPages/xsm ) {
                    delete $dependencies{$name};
                    my $msg = __(
'pdftk is installed, but cannot access the directory used for temporary files.'
                      )
                      . __(
'One reason for this might be that pdftk was installed via snap.'
                      )
                      . __(
'In this case, removing pdftk, and reinstalling without using snap would allow gscan2pdf to use pdftk.'
                      )
                      . __(
'Another workaround would be to select a temporary directory under your home directory in Edit/Preferences.'
                      );
                    show_message_dialog(
                        parent           => $window,
                        type             => 'warning',
                        buttons          => 'ok',
                        text             => $msg,
                        'store-response' => TRUE
                    );
                }
            }
        }
    }

    # OCR engine options
    if ( $dependencies{gocr} ) {
        push @ocr_engine,
          [ 'gocr', __('GOCR'), __('Process image with GOCR.') ];
    }
    if ( $dependencies{tesseract} ) {
        push @ocr_engine,
          [ 'tesseract', __('Tesseract'), __('Process image with Tesseract.') ];
    }
    if ( $dependencies{ocropus} ) {
        $logger->info('Found ocropus');
        push @ocr_engine,
          [ 'ocropus', __('Ocropus'), __('Process image with Ocropus.') ];
    }
    if ( $dependencies{cuneiform} ) {
        $logger->info("Found cuneiform v$dependencies{cuneiform}");
        push @ocr_engine,
          [ 'cuneiform', __('Cuneiform'), __('Process image with Cuneiform.') ];
    }

    # Build a look-up table of all true-type fonts installed
    my ( undef, $stdout ) =
      Gscan2pdf::Document::exec_command( ['fc-list : family style file'] );
    $stdout = Encode::decode_utf8($stdout);
    for ( split /\n/sm, $stdout ) {
        if (/ttf:[ ]/xsm) {
            my ( $file, $family, $style ) = split /:/xsm;
            chomp $style;
            $family =~ s/^[ ]//xsm;
            $family =~ s/,.*$//xsm;
            $style =~ s/^style=//xsm;
            $style =~ s/,.*$//xsm;
            $fonts->{by_file}{$file} = [ $family, $style ];
            $fonts->{by_family}{$family}{$style} = $file;
        }
    }
    return;
}

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

sub create_menu_bar {

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

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

    my @action_items = (

        # Fields for each action item:
        # [name, stock_id, value, label, accelerator, tooltip, callback]

        # File menu
        [ 'File', undef, __('_File') ],
        [
            'New',                  'gtk-new',
            __('_New'),             '<control>n',
            __('Clears all pages'), \&new
        ],
        [
            'Open',                   'gtk-open',
            __('_Open'),              '<control>o',
            __('Open image file(s)'), \&open_dialog
        ],
        [
            'Open crashed session',      undef,
            __('Open c_rashed session'), undef,
            __('Open crashed session'),  \&open_session_action
        ],
        [
            'Scan',              'scanner',
            __('S_can'),         '<control>g',
            __('Scan document'), \&scan_dialog
        ],
        [
            'Save',     'gtk-save', __('Save'), '<control>s',
            __('Save'), \&save_dialog
        ],
        [
            'Email as PDF',                     'mail-attach',
            __('_Email as PDF'),                '<control>e',
            __('Attach as PDF to a new email'), \&email
        ],
        [
            'Print',     'gtk-print', __('_Print'), '<control>p',
            __('Print'), \&print_dialog
        ],
        [
            'Compress',                      undef,
            __('_Compress temporary files'), undef,
            __('Compress temporary files'),  \&compress_temp
        ],
        [
            'Quit',
            'gtk-quit',
            __('_Quit'),
            '<control>q',
            __('Quit'),
            sub {
                if ( quit() ) { $app->quit }
            }
        ],

        # Edit menu
        [ 'Edit', undef, __('_Edit') ],
        [ 'Undo', 'gtk-undo', __('_Undo'), '<control>z', __('Undo'), \&undo ],
        [
            'Redo',      'gtk-redo',
            __('_Redo'), '<shift><control>z',
            __('Redo'),  \&unundo
        ],
        [
            'Cut',               'gtk-cut',
            __('Cu_t'),          '<control>x',
            __('Cut selection'), \&cut_selection
        ],
        [
            'Copy',               'gtk-copy',
            __('_Copy'),          '<control>c',
            __('Copy selection'), \&copy_selection
        ],
        [
            'Paste',               'gtk-paste',
            __('_Paste'),          '<control>v',
            __('Paste selection'), \&paste_selection
        ],
        [
            'Delete',                    'gtk-delete',
            __('_Delete'),               undef,
            __('Delete selected pages'), \&delete_selection
        ],
        [
            'Renumber',           'gtk-sort-ascending',
            __('_Renumber'),      '<control>r',
            __('Renumber pages'), \&renumber_dialog
        ],
        [ 'Select', undef, __('_Select') ],
        [
            'Select All',           'gtk-select-all',
            __('_All'),             '<control>a',
            __('Select all pages'), \&select_all
        ],
        [
            'Select Odd', undef, __('_Odd'), '<control>1',
            __('Select all odd-numbered pages'),
            sub { select_odd_even(0); }
        ],
        [
            'Select Even', undef, __('_Even'), '<control>2',
            __('Select all evenly-numbered pages'),
            sub { select_odd_even(1); }
        ],
        [
            'Select Blank',
            'gtk-select-blank',
            __('_Blank'),
            '<control>b',
            __('Select pages with low standard deviation'),
            \&analyse_select_blank
        ],
        [
            'Select Dark',           'gtk-select-blank',
            __('_Dark'),             '<control>d',
            __('Select dark pages'), \&analyse_select_dark
        ],
        [
            'Select Modified',
            'gtk-select-modified',
            __('_Modified'),
            '<control>m',
            __('Select modified pages since last OCR'),
            \&select_modified_since_ocr
        ],
        [
            'Select No OCR',                       undef,
            __('_No OCR'),                         undef,
            __('Select pages with no OCR output'), \&select_no_ocr
        ],
        [
            'Clear OCR',                                'gtk-clear',
            __('_Clear OCR'),                           undef,
            __('Clear OCR output from selected pages'), \&clear_ocr
        ],
        [
            'Properties',                'gtk-properties',
            __('Propert_ies'),           undef,
            __('Edit image properties'), \&properties
        ],
        [
            'Preferences',          'gtk-preferences',
            __('Prefere_nces'),     undef,
            __('Edit preferences'), \&preferences
        ],

        # View menu
        [ 'View', undef, __('_View') ],
        [
            'Zoom 100',       'gtk-zoom-100',
            __('Zoom _100%'), undef,
            __('Zoom to 100%'), sub { $view->set_zoom(1.0) }
        ],
        [
            'Zoom to fit',      'gtk-zoom-fit',
            __('Zoom to _fit'), undef,
            __('Zoom to fit'), sub { $view->zoom_to_fit }
        ],
        [
            'Zoom in',      'gtk-zoom-in',
            __('Zoom _in'), 'plus',
            __('Zoom in'), sub { $view->zoom_in }
        ],
        [
            'Zoom out',      'gtk-zoom-out',
            __('Zoom _out'), 'minus',
            __('Zoom out'), sub { $view->zoom_out }
        ],
        [
            'Rotate 90',
            'rotate90',
            __('Rotate 90° clockwise'),
            '<control><shift>R',
            __('Rotate 90° clockwise'),
            sub {
                rotate( $_90_DEGREES,
                    [ indices2pages( $slist->get_selected_indices ) ] );
            }
        ],
        [
            'Rotate 180',
            'rotate180',
            __('Rotate 180°'),
            '<control><shift>F',
            __('Rotate 180°'),
            sub {
                rotate( $_180_DEGREES,
                    [ indices2pages( $slist->get_selected_indices ) ] );
            }
        ],
        [
            'Rotate 270',
            'rotate270',
            __('Rotate 90° anticlockwise'),
            '<control><shift>C',
            __('Rotate 90° anticlockwise'),
            sub {
                rotate( $_270_DEGREES,
                    [ indices2pages( $slist->get_selected_indices ) ] );
            }
        ],

        # Tools menu
        [ 'Tools', undef, __('_Tools') ],
        [
            'Threshold', undef, __('_Threshold'), undef,
            __('Change each pixel above this threshold to black'),
            \&threshold
        ],
        [
            'BrightnessContrast',               undef,
            __('_Brightness / Contrast'),       undef,
            __('Change brightness & contrast'), \&brightness_contrast
        ],
        [
            'Negate', undef, __('_Negate'), undef,
            __('Converts black to white and vice versa'), \&negate
        ],
        [
            'Unsharp',                   undef,
            __('_Unsharp Mask'),         undef,
            __('Apply an unsharp mask'), \&unsharp
        ],
        [
            'CropDialog',     'GTK_STOCK_LEAVE_FULLSCREEN',
            __('_Crop'),      undef,
            __('Crop pages'), \&crop_dialog
        ],
        [
            'CropSelection',      'selection',
            __('_Crop'),          undef,
            __('Crop selection'), \&crop_selection
        ],
        [
            'unpaper', undef, __('_Clean up'), undef,
            __('Clean up scanned images with unpaper'), \&unpaper
        ],
        [
            'OCR', undef, __('_OCR'), undef,
            __('Optical Character Recognition'),
            \&ocr_dialog
        ],
        [
            'User-defined', undef, __('U_ser-defined'), undef,
            __('Process images with user-defined tool'),
            \&user_defined_dialog
        ],

        # Help menu
        [ 'Help menu', undef, __('_Help') ],
        [
            'Help',     'gtk-help', __('_Help'), '<control>h',
            __('Help'), \&view_html
        ],
        [ 'About', 'gtk-about', __('_About'), undef, __('_About'), \&about ],
    );

    my @viewer_tools = (
        [
            'Tabbed', undef, __('_Tabbed'), undef,
            __('Arrange image and OCR viewers in tabs'), $TABBED_VIEW
        ],
        [
            'Split', undef, __('_Split'), undef,
            __('Arrange image and OCR viewers in split screen'), $SPLIT_VIEW
        ],
    );

    my $ui = <<'EOS';
<ui>
 <menubar name='MenuBar'>
  <menu action='File'>
   <menuitem action='New'/>
   <menuitem action='Open'/>
   <menuitem action='Open crashed session'/>
   <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='Tabbed'/>
   <menuitem action='Split'/>
   <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='BrightnessContrast'/>
   <menuitem action='Negate'/>
   <menuitem action='Unsharp'/>
   <menuitem action='CropDialog'/>
   <separator/>
   <menuitem action='unpaper'/>
   <menuitem action='OCR'/>
   <separator/>
   <menuitem action='User-defined'/>
  </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='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='CropSelection'/>
  <separator/>
  <toolitem action='Help'/>
  <toolitem action='Quit'/>
 </toolbar>
 <popup name='Detail_Popup'>
  <menuitem action='Zoom 100'/>
  <menuitem action='Zoom to fit'/>
  <menuitem action='Zoom in'/>
  <menuitem action='Zoom out'/>
  <separator/>
  <menuitem action='Rotate 90'/>
  <menuitem action='Rotate 180'/>
  <menuitem action='Rotate 270'/>
  <separator/>
  <menuitem action='CropSelection'/>
  <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='CropSelection'/>
  <separator/>
  <menuitem action='Cut'/>
  <menuitem action='Copy'/>
  <menuitem action='Paste'/>
  <menuitem action='Delete'/>
  <separator/>
  <menuitem action='Clear OCR'/>
  <separator/>
  <menuitem action='Properties'/>
 </popup>
</ui>
EOS

    # Create the basic Gtk3::ActionGroup instance
    # and fill it with Gtk3::Action instances
    my $actions_basic = Gtk3::ActionGroup->new('actions_basic');
    $actions_basic->add_actions( \@action_items, undef );
    $actions_basic->add_radio_actions( \@viewer_tools, $SETTING{viewer_tools},
        \&change_view_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
    $menubar = $uimanager->get_widget('/MenuBar');

    # Check for presence of various packages
    check_dependencies();

    # Ghost save image item if imagemagick not available
    my $msg = $EMPTY;
    if ( not $dependencies{imagemagick} ) {
        $msg .= __("Save image and Save as PDF both require imagemagick\n");
    }

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

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

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

    # 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/BrightnessContrast')
      ->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/CropDialog')->set_sensitive(FALSE);
    $uimanager->get_widget('/MenuBar/Tools/User-defined')->set_sensitive(FALSE);

    # Ghost rotations and unpaper if perlmagick not available
    if ( not $dependencies{perlmagick} ) {
        $msg .=
          __("The rotating options and unpaper support require perlmagick\n");
    }

    if ( not $dependencies{unpaper} ) {
        $msg .= __("unpaper missing\n");
    }

    # Ghost ocr item if ocr not available
    # Brackets required, as otherwise = would have higher precedence. See
    # http://perldoc.perl.org/perlop.html#Logical-or-and-Exclusive-Or
    $dependencies{ocr} = (
             $dependencies{gocr}
          or $dependencies{tesseract}
          or $dependencies{ocropus}
          or $dependencies{cuneiform}
    );
    if ( not $dependencies{ocr} ) {
        $msg .= __("OCR requires gocr, tesseract, ocropus, or cuneiform\n");
    }

    if ( not $dependencies{pdftk} ) {
        $msg .= __("PDF encryption requires pdftk\n");
    }

    # Put up warning if needed
    if ( $msg ne $EMPTY ) {
        $msg = __('Warning: missing packages') . "\n$msg";
        show_message_dialog(
            parent           => $window,
            type             => 'warning',
            buttons          => 'ok',
            text             => $msg,
            'store-response' => TRUE
        );
    }

    # extract the toolbar
    $toolbar = $uimanager->get_widget('/ToolBar');

    # turn off labels
    my $settings = $toolbar->get_settings();
    $settings->set( 'gtk-toolbar-style', 'icons' );    # only icons

    return;
}

# ghost or unghost as necessary as # pages > 0 or not.

sub update_uimanager {
    my @widgets = (
        '/MenuBar/View/Tabbed',
        '/MenuBar/View/Split',
        '/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/BrightnessContrast',
        '/MenuBar/Tools/Negate',
        '/MenuBar/Tools/Unsharp',
        '/MenuBar/Tools/CropDialog',
        '/MenuBar/Tools/unpaper',
        '/MenuBar/Tools/OCR',
        '/MenuBar/Tools/User-defined',

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

        '/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',
        '/Detail_Popup/CropSelection',

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

    if ( $slist->get_selected_indices ) {
        for (@widgets) {
            $uimanager->get_widget($_)->set_sensitive(TRUE);
        }
    }
    else {
        for (@widgets) {
            $uimanager->get_widget($_)->set_sensitive(FALSE);
        }
    }

    # Ghost rotations and unpaper if perlmagick not available
    if ( not $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);
    }

    # Ghost unpaper item if unpaper not available
    if ( not $dependencies{unpaper} ) {
        $uimanager->get_widget('/MenuBar/Tools/unpaper')->set_sensitive(FALSE);
    }

    # Ghost ocr item if ocr  not available
    if ( not $dependencies{ocr} ) {
        $uimanager->get_widget('/MenuBar/Tools/OCR')->set_sensitive(FALSE);
    }

    if ( $#{ $slist->{data} } > $EMPTY_LIST ) {
        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);
        if ( defined $save_button ) { $save_button->set_sensitive(TRUE) }
    }
    else {
        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);
            if ( defined $windowe ) { $windowe->hide }
        }
        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);
        if ( defined $save_button ) { $save_button->set_sensitive(FALSE) }
    }

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

sub selection_changed_callback {
    my @page = $slist->get_selected_indices;

    # Display the new image
    if (@page) {
        my $path = Gtk3::TreePath->new_from_indices( $page[0] );
        $slist->scroll_to_cell( $path, $slist->get_column(0),
            TRUE, $HALF, $HALF );
        my $sel = $view->get_selection;
        display_image( $slist->{data}[ $page[0] ][2] );
        if ( defined $sel ) { $view->set_selection($sel) }
    }
    else {
        $view->set_pixbuf(undef);
        $canvas->clear_text;
        undef $current_page;
    }
    update_uimanager();
    return;
}

sub drag_motion_callback {
    my ( $tree, $context, $x, $y, $t ) = @_;
    my ( $path, $how ) = $tree->get_dest_row_at_pos( $x, $y ) or return;
    my $scroll = $tree->get_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->get_actions ==    ## no critic (ProhibitMismatchedOperators)
        'copy'
      )
    {
        @action = ('copy');
    }
    else {
        @action = ('move');
    }
    Gtk3::Gdk::drag_status( $context, @action, $t );

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

    if ( $y > $adj->get_page_size - $step / 2 ) {
        my $v = $value + $step;
        my $m = $adj->get_upper - $adj->get_page_size;
        $adj->set_value( $v > $m ? $m : $v );
    }
    elsif ( $y < $step / 2 ) {
        my $v = $value - $step;
        my $m = $adj->get_lower;
        $adj->set_value( $v < $m ? $m : $v );
    }

    return FALSE;
}

sub create_temp_directory {
    find_crashed_sessions();

    # Create temporary directory if necessary
    if ( not defined $session ) {
        if ( defined $SETTING{TMPDIR} and $SETTING{TMPDIR} ne $EMPTY ) {
            if ( not -d $SETTING{TMPDIR} ) { mkdir $SETTING{TMPDIR} }
            try {
                $session =
                  File::Temp->newdir( 'gscan2pdf-XXXX',
                    DIR => $SETTING{TMPDIR} );
            }
            catch {
                $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
                $logger->warn(
                    sprintf __(
'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 );
        }
        $slist->set_dir($session);
        open $lockfh, '>',    ## no critic (RequireBriefOpen)
          File::Spec->catfile( $session, 'lockfile' )
          or die "Cannot open lockfile\n";
        flock $lockfh, LOCK_EX or die "Cannot lock file\n";
        $slist->save_session;
        $logger->info("Using $session for temporary files");
    }
    return;
}

sub find_crashed_sessions {

    # Look for crashed sessions
    if ( defined $SETTING{TMPDIR} and $SETTING{TMPDIR} ne $EMPTY ) {
        $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
              or die "Unlocking error on $lockfh ($ERRNO)\n";
            close $lockfh or warn "Error closing $lockfh ($ERRNO)\n";
        }
    }

    # Flag those with no session file
    my @missing;
    for ( 0 .. $#crashed ) {
        if ( not -r File::Spec->catfile( $crashed[$_], 'session' ) ) {
            push @missing, $crashed[$_];
            splice @crashed, $_, 1;
        }
    }
    if (@missing) {
        my $dialog = Gtk3::Dialog->new(
            __('Crashed sessions'),
            $window, 'modal',
            'gtk-delete' => 'ok',
            'gtk-cancel' => 'cancel'
        );
        my $text = Gtk3::TextView->new;
        $text->set_wrap_mode('word');
        $text->get_buffer->set_text(
            __(
                    'The following list of sessions cannot be restored.'
                  . ' Please retrieve any images you require from them.'
                  . ' Selected sessions will be deleted.'
            )
        );
        $dialog->get_content_area->add($text);
        my $sessionlist = Gtk3::SimpleList->new( __('Session') => 'text', );
        push @{ $sessionlist->{data} }, @missing;
        $dialog->get_content_area->add($sessionlist);
        $dialog->show_all;

        if ( $dialog->run eq 'ok' ) {
            my @selected = $sessionlist->get_selected_indices;
            for (@selected) { $_ = $missing[$_] }
            if (@selected) { remove_tree(@selected) }
        }
        $dialog->destroy;
    }

    # Allow user to pick a crashed session to restore
    if (@crashed) {
        my $dialog = Gtk3::Dialog->new(
            __('Pick crashed session to restore'),
            $window, 'modal',
            'gtk-ok'     => 'ok',
            'gtk-cancel' => 'cancel'
        );
        my $label = Gtk3::Label->new( __('Pick crashed session to restore') );
        my $box   = $dialog->get_content_area;
        $box->add($label);
        my $sessionlist = Gtk3::SimpleList->new( __('Session') => 'text', );
        push @{ $sessionlist->{data} }, @crashed;
        $box->add($sessionlist);
        $dialog->show_all;

        if ( $dialog->run eq 'ok' ) {
            ($selected) = $sessionlist->get_selected_indices;
        }
        $dialog->destroy;

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

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

    $current_page = $page;

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

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

    # Update the ranges on the crop dialog
    if ( defined $sb_selector_w and defined $current_page ) {
        $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}{x}      = $sb_selector_x->get_value;
        $SETTING{selection}{y}      = $sb_selector_y->get_value;
        $SETTING{selection}{width}  = $sb_selector_w->get_value;
        $SETTING{selection}{height} = $sb_selector_h->get_value;
        $view->set_selection( $SETTING{selection} );
    }

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

sub create_canvas {
    my ($page) = @_;
    $canvas->clear_text;
    $canvas->add_text( $page, \&edit_ocr_text );
    $canvas->set_scale( $view->get_zoom );
    my $offset = $view->get_offset;
    $canvas->set_offset( $offset->{x}, $offset->{y} );
    $canvas->show;
    return;
}

sub edit_ocr_text {
    my ( $widget, $target, $ev ) = @_;
    my $dialog = Gtk3::Dialog->new(
        __('Editing text') . '...', $window,
        'modal',
        'gtk-ok'     => 'ok',
        'gtk-cancel' => 'cancel'
    );
    my $textview   = Gtk3::TextView->new;
    my $textbuffer = $textview->get_buffer;
    $textbuffer->set( text => $widget->get('text') );
    $dialog->get_content_area->add($textview);
    $dialog->set_default_response('ok');
    $dialog->show_all;

    if ( $dialog->run eq 'ok' ) {
        $canvas->set_box_text( $widget, $textbuffer->get('text') );
        $current_page->{hocr} = $canvas->hocr;
    }
    $dialog->destroy;
    $canvas->pointer_ungrab( $widget, $ev->time );
    return TRUE;
}

# Check that all pages have been saved

sub scans_saved {
    my ($message) = @_;
    if ( not $slist->scans_saved ) {
        my $response = ask_question(
            parent             => $window,
            type               => 'question',
            buttons            => 'ok-cancel',
            text               => $message,
            'store-response'   => TRUE,
            'stored-responses' => ['ok']
        );
        if ( $response ne 'ok' ) { return FALSE }
    }
    return TRUE;
}

# Deletes all scans after warning.

sub new {

    if (
        not scans_saved(
            __(
"Some pages have not been saved.\nDo you really want to clear all pages?"
            )
        )
      )
    {
        return;
    }

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

    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 = Gtk3::FileFilter->new;
    for my $extension (@file_extensions) {
        my @filter_pattern;

        # Create case insensitive pattern
        for my $byte ( split $EMPTY, $extension ) {
            push @filter_pattern, '[' . uc($byte) . lc($byte) . ']';
        }
        my $new_filter_pattern = join $EMPTY, @filter_pattern;
        $filter->add_pattern( q{*.} . $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 = Gtk3::FileFilter->new;
    $filter->add_pattern(q{*});
    $filter->set_name('All files');
    $file_chooser->add_filter($filter);
    return;
}

sub error_callback {
    my ( $page_uuid, $process, $message ) = @_;
    $logger->error("$page_uuid, $process, $message");

    my $page = $slist->find_page_by_uuid($page_uuid);
    show_message_dialog(
        parent           => $window,
        type             => 'error',
        buttons          => 'close',
        page             => $slist->{data}[$page][0],
        process          => $process,
        text             => $message,
        'store-response' => TRUE
    );
    $thbox->hide;
    return;
}

sub open_session_file {
    my ($filename) = @_;
    $logger->info("Restoring session in $session");
    $slist->open_session_file(
        info           => $filename,
        error_callback => \&error_callback
    );
    return;
}

sub open_session_action {
    my ($action) = @_;
    my $file_chooser = Gtk3::FileChooserDialog->new(
        __('Open crashed session'),
        $window, 'select-folder',
        'gtk-cancel' => 'cancel',
        'gtk-ok'     => 'ok'
    );
    $file_chooser->set_default_response('ok');
    $file_chooser->set_current_folder( $SETTING{cwd} );

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

        # Update undo/redo buffers
        take_snapshot();

        my @filename = $file_chooser->get_filenames;
        open_session( $filename[0] );
    }
    $file_chooser->destroy;
    return;
}

sub open_session {
    my ($sesdir) = @_;
    $logger->info("Restoring session in $session");
    $slist->open_session(
        dir            => $sesdir,
        delete         => FALSE,
        error_callback => \&error_callback
    );
    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 __('Process %i of %i (%s)'),
            $completed + 1,
            $total, $process
        );
        $tpbar->set_fraction( ( $completed + $HALF ) / $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->cancel( [$pid] );
                $thbox->hide;
            }
        );
    }
    return;
}

# Helper function to update thread progress bar

sub update_tpbar {
    my (%options) = @_;
    if ( $options{jobs_total} ) {
        if ( defined $options{process} ) {
            if ( defined $options{message} ) {
                $options{process} .= " - $options{message}";
            }
            $tpbar->set_text(
                sprintf __('Process %i of %i (%s)'),
                $options{jobs_completed} + 1,
                $options{jobs_total}, $options{process}
            );
        }
        else {
            $tpbar->set_text(
                sprintf __('Process %i of %i'),
                $options{jobs_completed} + 1,
                $options{jobs_total}
            );
        }
        if ( defined $options{progress} ) {
            $tpbar->set_fraction(
                ( $options{jobs_completed} + $options{progress} ) /
                  $options{jobs_total} );
        }
        else {
            $tpbar->set_fraction(
                ( $options{jobs_completed} + $HALF ) / $options{jobs_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 = Gtk3::FileChooserDialog->new(
        __('Open image'),
        $window, 'open',
        'gtk-cancel' => 'cancel',
        'gtk-ok'     => 'ok'
    );
    $file_chooser->set_select_multiple(TRUE);
    $file_chooser->set_default_response('ok');
    $file_chooser->set_current_folder( $SETTING{cwd} );
    add_filter( $file_chooser, __('Image files'),
        'jpg', 'png', 'pnm', 'ppm', 'pbm', '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 $filenames = $file_chooser->get_filenames;
        $file_chooser->destroy;

        # Update cwd
        $SETTING{cwd} = dirname( $filenames->[0] );

        import_files($filenames);
    }
    else {
        $file_chooser->destroy;
    }

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

sub import_files {
    my ($filenames) = @_;

    # FIXME: import_files() now returns an array of pids.
    my ( $signal, $pid );
    $pid = $slist->import_files(
        paths             => $filenames,
        password_callback => sub {
            my ($filename) = @_;
            my $text = sprintf __('Enter user password for PDF %s'), $filename;
            my $dialog =
              Gtk3::MessageDialog->new( $window,
                [ 'destroy-with-parent', 'modal' ],
                'question', 'ok-cancel', $text );
            $dialog->set_title($text);
            my $vbox  = $dialog->get_content_area;
            my $entry = Gtk3::Entry->new;
            $entry->set_visibility(FALSE);
            $entry->set_invisible_char($ASTERISK);
            $vbox->pack_end( $entry, FALSE, FALSE, 0 );
            $dialog->show_all;
            my $response = $dialog->run;
            $text = $entry->get_text;
            $dialog->destroy;

            if ( $response eq 'ok' and $text ne $EMPTY ) {
                return $text;
            }
            return;
        },
        queued_callback => sub {
            $logger->debug("import_files queued @{$filenames}");
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $logger->debug("import_files started @{$filenames}");
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        pagerange_callback => sub {
            my ($info) = @_;
            my $dialog = Gtk3::Dialog->new(
                __('Pages to extract'),
                $window, [qw/modal destroy-with-parent/],
                'gtk-ok'     => 'ok',
                'gtk-cancel' => 'cancel'
            );
            my $vbox = $dialog->get_content_area;
            my $hbox = Gtk3::HBox->new;
            $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
            my $label = Gtk3::Label->new( __('First page to extract') );
            $hbox->pack_start( $label, FALSE, FALSE, 0 );
            my $spinbuttonf =
              Gtk3::SpinButton->new_with_range( 1, $info->{pages}, 1 );
            $hbox->pack_end( $spinbuttonf, FALSE, FALSE, 0 );
            $hbox = Gtk3::HBox->new;
            $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
            $label = Gtk3::Label->new( __('Last page to extract') );
            $hbox->pack_start( $label, FALSE, FALSE, 0 );
            my $spinbuttonl =
              Gtk3::SpinButton->new_with_range( 1, $info->{pages}, 1 );
            $spinbuttonl->set_value( $info->{pages} );
            $hbox->pack_end( $spinbuttonl, FALSE, FALSE, 0 );

            $dialog->show_all;
            my $response = $dialog->run;
            $dialog->destroy;
            if ( $response eq 'ok' ) {
                return $spinbuttonf->get_value, $spinbuttonl->get_value;
            }
            return;
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ($pending) = @_;
            $logger->debug("import_files finished @{$filenames}");
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            $slist->save_session;
        },
        error_callback => \&error_callback
    );
    return;
}

# Get metadata
sub update_metadata_settings {
    my ($dialog) = @_;

    for my $name (qw(author title subject keywords)) {
        $SETTING{$name} = $dialog->get("meta-$name");
        $SETTING{"$name-suggestions"} = $dialog->get("meta-$name-suggestions");
    }
    my $datetime = $dialog->get('meta-datetime');
    my $success  = TRUE;
    try {
        $SETTING{'datetime offset'} =
          [ Delta_DHMS( Today_and_Now(), @{$datetime} ) ];
    }
    catch {
        $success = FALSE;
        my $msg =
          sprintf __('%04d-%02d-%02d %02d:%02d:%02d is not a valid datetime.'),
          @{$datetime};
        $logger->debug($msg);
        show_message_dialog(
            parent  => $window,
            type    => 'error',
            buttons => 'close',
            text    => $msg,
        );
    };
    return $success;
}

# Save selected pages as PDF under given name.

sub save_pdf {
    my ( $filename, $option, $list_of_pages ) = @_;

    # Compile options
    my %options = (
        compression      => $SETTING{'pdf compression'},
        downsample       => $SETTING{downsample},
        'downsample dpi' => $SETTING{'downsample dpi'},
        quality          => $SETTING{quality},
        font             => $SETTING{'pdf font'},
        'user-password'  => $windowi->get('pdf-user-password'),
        set_timestamp    => $SETTING{set_timestamp},
        'convert whitespace to underscores' =>
          $SETTING{'convert whitespace to underscores'},
    );
    if ( $option eq 'prependpdf' ) {
        $options{prepend} = $filename;
    }
    elsif ( $option eq 'appendpdf' ) {
        $options{append} = $filename;
    }
    elsif ( $option eq 'ps' ) {
        $options{ps}     = $filename;
        $options{pstool} = $SETTING{ps_backend};
    }
    if ( $SETTING{post_save_hook} ) {
        $options{post_save_hook} = $SETTING{current_psh};
    }

    # Create the PDF
    $logger->debug("Started saving $filename");
    my ( $signal, $pid );
    $pid = $slist->save_pdf(
        path          => "$filename",      # stringify in case of PS
        list_of_pages => $list_of_pages,
        metadata => Gscan2pdf::Document::collate_metadata(
            \%SETTING,
            [ Today_and_Now() ],
            [ Timezone() ]
        ),
        options         => \%options,
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            mark_pages($list_of_pages);
            if ( defined $SETTING{'view files toggle'}
                and $SETTING{'view files toggle'} )
            {
                if ( defined $options{ps} ) {
                    system "xdg-open \"$options{ps}\" &";
                }
                else {
                    system "xdg-open \"$filename\" &";
                }
            }
            $logger->debug("Finished saving $filename");
        },
        error_callback => \&error_callback
    );
    return;
}

# Display page selector and on save a fileselector.

sub save_dialog {

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

    my @image_types = qw(pdf gif jpg png pnm ps tif txt hocr session);
    if ( $dependencies{pdfunite} ) {
        push @image_types, 'prependpdf', 'appendpdf';
    }
    if ( $dependencies{djvu} ) { push @image_types, 'djvu' }
    my @ps_backends;
    for my $backend (qw(libtiff pdf2ps pdftops)) {
        if ( $dependencies{$backend} ) { push @ps_backends, $backend }
    }
    $windowi = Gscan2pdf::Dialog::Save->new(
        'transient-for'  => $window,
        title            => __('Save'),
        'hide-on-delete' => TRUE,
        'page-range'     => $SETTING{'Page range'},
        'include-time'   => $SETTING{use_time},
        'meta-datetime'  => [
            Add_Delta_DHMS( Today_and_Now(), @{ $SETTING{'datetime offset'} } )
        ],

        # TRUE if any value is non-zero
        'select-datetime' =>
          scalar( grep { !/^0$/xsm } @{ $SETTING{'datetime offset'} } ),
        'meta-title'                => $SETTING{'title'},
        'meta-title-suggestions'    => $SETTING{'title-suggestions'},
        'meta-author'               => $SETTING{'author'},
        'meta-author-suggestions'   => $SETTING{'author-suggestions'},
        'meta-subject'              => $SETTING{'subject'},
        'meta-subject-suggestions'  => $SETTING{'subject-suggestions'},
        'meta-keywords'             => $SETTING{'keywords'},
        'meta-keywords-suggestions' => $SETTING{'keywords-suggestions'},
        'image-types'               => \@image_types,
        'image-type'                => $SETTING{'image type'},
        'ps-backends'               => \@ps_backends,
        'jpeg-quality'              => $SETTING{quality},
        'downsample-dpi'            => $SETTING{'downsample dpi'},
        downsample                  => $SETTING{downsample},
        'pdf-compression'           => $SETTING{'pdf compression'},
        'available-fonts'           => $fonts,
        'pdf-font'                  => $SETTING{'pdf font'},
        'can-encrypt-pdf'           => defined $dependencies{pdftk},
        'tiff-compression'          => $SETTING{'tiff compression'},
    );

    # Frame for page range
    $windowi->add_page_range;

    $windowi->add_image_type;

    # Post-save hook
    my $pshbutton = Gtk3::CheckButton->new( __('Post-save hook') );
    $pshbutton->set_tooltip_text(
        __(
'Run command on saved file. The available commands are those user-defined tools that do not specify %o'
        )
    );
    my $vbox = $windowi->get_content_area;
    $vbox->pack_start( $pshbutton, FALSE, TRUE, 0 );
    update_post_save_hooks();
    $vbox->pack_start( $windowi->{comboboxpsh}, FALSE, TRUE, 0 );
    $pshbutton->signal_connect(
        toggled => sub {
            $windowi->{comboboxpsh}->set_sensitive( $pshbutton->get_active );
        }
    );
    $pshbutton->set_active( $SETTING{post_save_hook} );
    $windowi->{comboboxpsh}->set_sensitive( $pshbutton->get_active );

    my $kbutton = Gtk3::CheckButton->new( __('Close dialog on save') );
    $kbutton->set_tooltip_text( __('Close dialog on save') );
    $kbutton->set_active( $SETTING{close_dialog_on_save} );
    $vbox->pack_start( $kbutton, FALSE, TRUE, 0 );

    $windowi->add_actions( 'gtk-save',
        sub { save_button_clicked_callback( $kbutton, $pshbutton ) },
        'gtk-cancel', sub { $windowi->hide } );

    $windowi->show_all;
    $windowi->resize( 1, 1 );
    return;
}

sub list_of_pages {

    # Compile list of pages
    my @list_of_pages;
    my @pagelist =
      $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
    if ( not @pagelist ) { return }
    for (@pagelist) {
        push @list_of_pages, $slist->{data}[$_][2]->{uuid};
    }
    return \@list_of_pages;
}

sub save_button_clicked_callback {
    my ( $kbutton, $pshbutton ) = @_;

    # Compile list of pages
    $SETTING{'Page range'} = $windowi->get('page-range');
    my $list_of_pages = list_of_pages;

    # dig out the image type, compression and quality
    $SETTING{'image type'} = $windowi->get('image-type');
    $SETTING{close_dialog_on_save} = $kbutton->get_active;

    $SETTING{post_save_hook} = $pshbutton->get_active;
    if (    $SETTING{post_save_hook}
        and $windowi->{comboboxpsh}->get_active > $EMPTY_LIST )
    {
        $SETTING{current_psh} = $windowi->{comboboxpsh}->get_active_text;
    }

    given ( $SETTING{'image type'} ) {
        when (/pdf/xsm) {

            # dig out the compression
            $SETTING{downsample}        = $windowi->get('downsample');
            $SETTING{'downsample dpi'}  = $windowi->get('downsample-dpi');
            $SETTING{'pdf compression'} = $windowi->get('pdf-compression');
            $SETTING{quality}           = $windowi->get('jpeg-quality');

            $SETTING{'pdf font'} = $windowi->get('pdf-font');

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

            my $file_chooser;
            if ( $_ eq 'pdf' ) {
                if ( not update_metadata_settings($windowi) ) {
                    save_dialog();
                    return;
                }

                # Set up file selector
                $file_chooser = Gtk3::FileChooserDialog->new(
                    __('PDF filename'),
                    $windowi, 'save',
                    'gtk-cancel' => 'cancel',
                    'gtk-save'   => 'ok'
                );

                my $filename = Gscan2pdf::Document::expand_metadata_pattern(
                    template => $SETTING{'default filename'},
                    convert_whitespace =>
                      $SETTING{'convert whitespace to underscores'},
                    author        => $SETTING{author},
                    title         => $SETTING{title},
                    docdate       => $windowi->get('meta-datetime'),
                    today_and_now => [ Today_and_Now() ],
                    extension     => 'pdf',
                );

                $file_chooser->set_current_name($filename);
                $file_chooser->set_do_overwrite_confirmation(TRUE);
            }
            else {
                $file_chooser = Gtk3::FileChooserDialog->new(
                    __('PDF filename'),
                    $windowi, 'open',
                    'gtk-cancel' => 'cancel',
                    'gtk-open'   => 'ok'
                );
            }
            add_filter( $file_chooser, __('PDF files'), 'pdf' );
            $file_chooser->set_current_folder( $SETTING{cwd} );
            $file_chooser->set_default_response('ok');
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ $_, $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('djvu') {

            if ( not update_metadata_settings($windowi) ) {
                save_dialog();
                return;
            }

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('DjVu filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );

            my $filename = Gscan2pdf::Document::expand_metadata_pattern(
                template => $SETTING{'default filename'},
                convert_whitespace =>
                  $SETTING{'convert whitespace to underscores'},
                author        => $SETTING{author},
                title         => $SETTING{title},
                docdate       => $windowi->get('meta-datetime'),
                today_and_now => [ Today_and_Now() ],
                extension     => 'djvu',
            );

            $file_chooser->set_current_name($filename);
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            add_filter( $file_chooser, __('DjVu files'), 'djvu' );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ 'djvu', $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('tif') {
            $SETTING{'tiff compression'} = $windowi->get('tiff-compression');
            $SETTING{quality} = $windowi->get('jpeg-quality');

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('TIFF filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            add_filter( $file_chooser, __('Image files'),
                $SETTING{'image type'} );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ 'tif', $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('txt') {

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('Text filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            add_filter( $file_chooser, __('Text files'), 'txt' );
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ 'txt', $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('hocr') {

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('hOCR filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            add_filter( $file_chooser, __('hOCR files'), 'hocr' );
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ 'hocr', $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('ps') {
            $SETTING{ps_backend} = $windowi->get('ps-backend');

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('PS filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            add_filter( $file_chooser, __('Postscript files'), 'ps' );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                [ 'ps', $list_of_pages ]
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('session') {

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

            # Set up file selector
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('gscan2pdf session filename'),
                $windowi, 'save',
                'gtk-cancel' => 'cancel',
                'gtk-save'   => 'ok'
            );
            $file_chooser->set_default_response('ok');
            $file_chooser->set_current_folder( $SETTING{cwd} );
            add_filter( $file_chooser, __('gscan2pdf session files'), 'gs2p' );
            $file_chooser->set_do_overwrite_confirmation(TRUE);
            $file_chooser->signal_connect(
                response => \&file_chooser_response_callback,
                ['gs2p']
            );
            $file_chooser->show;

            # cd back to tempdir
            chdir $session;
        }
        when ('jpg') {
            $SETTING{quality} = $windowi->get('jpeg-quality');
            save_image($list_of_pages);
        }
        default {
            save_image($list_of_pages);
        }
    }
    return;
}

sub file_chooser_response_callback {
    my ( $dialog, $response, $data ) = @_;
    my ( $type, $list_of_pages ) = @{$data};
    $logger->debug("save filename dialog returned $response");

    my $suffix = $type;
    if ( $suffix =~ /pdf/ixsm ) { $suffix = 'pdf' }
    if ( $response eq 'ok' ) {
        my $filename = $dialog->get_filename;
        $logger->debug("FileChooserDialog returned $filename");
        if ( $filename !~ /[.]$suffix$/ixsm ) {

            # a filename returned by Gtk3::FileChooserDialog containing utf8 is
            # not marked as utf8. This is then mangled by the append operation
            # below, but not for the operations than come afterwards, so just
            # turning on utf8 for the append.
            _utf8_on($filename);
            $filename = "$filename.$type";
            _utf8_off($filename);
            return if ( file_exists( $dialog, $filename ) );
        }

        return if ( file_writable( $dialog, $filename ) );

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

        given ($type) {
            when (/pdf/xsm) {
                save_pdf( $filename, $_, $list_of_pages );
            }
            when ('djvu') {
                save_djvu( $filename, $list_of_pages );
            }
            when ('tif') {
                save_tiff( $filename, undef, $list_of_pages );
            }
            when ('txt') {
                save_text( $filename, $list_of_pages );
            }
            when ('hocr') {
                save_hocr( $filename, $list_of_pages );
            }
            when ('ps') {
                if ( $SETTING{ps_backend} eq 'libtiff' ) {
                    my $tif =
                      File::Temp->new( DIR => $session, SUFFIX => '.tif' );
                    save_tiff( $tif->filename, $filename, $list_of_pages );
                }
                else {
                    save_pdf( $filename, 'ps', $list_of_pages );
                }
            }
            when ('gs2p') {
                $slist->save_session($filename);
            }
        }
        if ( defined $windowi and $SETTING{close_dialog_on_save} ) {
            $windowi->hide;
        }
    }
    $dialog->destroy;
    return;
}

sub file_exists {
    my ( $chooser, $filename ) = @_;
    if ( -f $filename ) {

        # File exists; get the file chooser to ask the user to confirm.
        $chooser->set_filename($filename);

        # Give the name change time to take effect.
        Glib::Idle->add( sub { $chooser->response('ok'); } );
        return TRUE;
    }
    return;
}

sub file_writable {
    my ( $chooser, $filename ) = @_;
    if ( not -w dirname($filename) ) {
        my $text = sprintf __('Directory %s is read-only'), dirname($filename);
        show_message_dialog(
            parent  => $chooser,
            type    => 'error',
            buttons => 'close',
            text    => $text
        );
        return TRUE;
    }
    elsif ( -f $filename and not -w $filename ) {
        my $text = sprintf __('File %s is read-only'), $filename;
        show_message_dialog(
            parent  => $chooser,
            type    => 'error',
            buttons => 'close',
            text    => $text
        );
        return TRUE;
    }
    return FALSE;
}

sub save_image {
    my ($list_of_pages) = @_;

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

    # Set up file selector
    my $file_chooser = Gtk3::FileChooserDialog->new(
        __('Image filename'),
        $windowi, 'save',
        'gtk-cancel' => 'cancel',
        'gtk-save'   => 'ok'
    );
    $file_chooser->set_default_response('ok');
    $file_chooser->set_current_folder( $SETTING{cwd} );
    add_filter( $file_chooser, __('Image files'),
        'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps' );
    $file_chooser->set_do_overwrite_confirmation(TRUE);

    if ( 'ok' eq $file_chooser->run ) {
        my $filename = $file_chooser->get_filename;

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

        # cd back to tempdir
        chdir $session;

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

        # Create the image
        $logger->debug("Started saving $filename");
        my ( $signal, $pid );
        $pid = $slist->save_image(
            path            => $filename,
            list_of_pages   => $list_of_pages,
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            running_callback => sub {
                return update_tpbar(@_);
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                mark_pages($list_of_pages);
                if ( defined $SETTING{'view files toggle'}
                    and $SETTING{'view files toggle'} )
                {
                    system "xdg-open \"$filename\" &";
                }
                $logger->debug("Finished saving $filename");
            },
            error_callback => \&error_callback
        );

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

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

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

    my ( $signal, $pid );
    $pid = $slist->save_tiff(
        path            => $filename,
        list_of_pages   => $list_of_pages,
        options         => \%options,
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            mark_pages($list_of_pages);
            my $file = defined $ps ? $ps : $filename;
            if ( defined $SETTING{'view files toggle'}
                and $SETTING{'view files toggle'} )
            {
                system "xdg-open \"$filename\" &";
            }
            $logger->debug("Finished saving $filename");
        },
        error_callback => \&error_callback
    );
    return;
}

sub save_djvu {
    my ( $filename, $list_of_pages ) = @_;

    # cd back to tempdir
    chdir $session;

    # Create the DjVu
    $logger->debug("Started saving $filename");
    my ( $signal, $pid );
    my %options = (
        set_timestamp => $SETTING{set_timestamp},
        'convert whitespace to underscores' =>
          $SETTING{'convert whitespace to underscores'},
    );
    if ( $SETTING{post_save_hook} ) {
        $options{post_save_hook} = $SETTING{current_psh};
    }
    $pid = $slist->save_djvu(
        path          => $filename,
        list_of_pages => $list_of_pages,
        options       => \%options,
        metadata      => Gscan2pdf::Document::collate_metadata(
            \%SETTING,
            [ Today_and_Now() ],
            [ Timezone() ]
        ),
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            mark_pages($list_of_pages);
            if ( defined $SETTING{'view files toggle'}
                and $SETTING{'view files toggle'} )
            {
                system "xdg-open \"$filename\" &";
            }
            $logger->debug("Finished saving $filename");
        },
        error_callback => \&error_callback
    );

    return;
}

sub save_text {
    my ( $filename, $list_of_pages ) = @_;

    my ( $signal, $pid, %options );
    if ( $SETTING{post_save_hook} ) {
        $options{post_save_hook} = $SETTING{current_psh};
    }
    $pid = $slist->save_text(
        path            => $filename,
        list_of_pages   => $list_of_pages,
        options         => \%options,
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            mark_pages($list_of_pages);
            if ( defined $SETTING{'view files toggle'}
                and $SETTING{'view files toggle'} )
            {
                system "xdg-open \"$filename\" &";
            }
            $logger->debug("Finished saving $filename");
        },
        error_callback => \&error_callback
    );
    return;
}

sub save_hocr {
    my ( $filename, $list_of_pages ) = @_;

    my ( $signal, $pid, %options );
    if ( $SETTING{post_save_hook} ) {
        $options{post_save_hook} = $SETTING{current_psh};
    }
    $pid = $slist->save_hocr(
        path            => $filename,
        list_of_pages   => $list_of_pages,
        options         => \%options,
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        running_callback => sub {
            return update_tpbar(@_);
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            mark_pages($list_of_pages);
            if ( defined $SETTING{'view files toggle'}
                and $SETTING{'view files toggle'} )
            {
                system "xdg-open \"$filename\" &";
            }
            $logger->debug("Finished saving $filename");
        },
        error_callback => \&error_callback
    );
    return;
}

sub mua {
    my ( $parent, $attachment ) = @_;

    # Check for thunderbird
    my ($client);
    if ( defined $ENV{KDE_FULL_SESSION}
        and $ENV{KDE_FULL_SESSION} eq 'true' )
    {
        ( undef, $client ) = Gscan2pdf::Document::exec_command(
            [
"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 $EMPTY
        )
        or ( defined $ENV{XDG_CURRENT_DESKTOP}
            and $ENV{XDG_CURRENT_DESKTOP} =~ /(:?XFCE|GNOME)/xsm )
      )
    {
        if ( not defined $gconftool ) {
            for (qw(gconftool gconftool-2)) {
                if ( Gscan2pdf::Document::check_command($_) ) {
                    $gconftool = $_;
                    last;
                }
            }
            if ( not defined $gconftool ) {
                show_message_dialog(
                    parent  => $parent,
                    type    => 'error',
                    buttons => 'close',
                    text    => __(
'You seem to be using Gnome or XFCE, but do not have gconftool installed. Unable to determine email client.'
                    )
                );
                return;
            }
        }

        ( undef, $client ) = Gscan2pdf::Document::exec_command(
            [
"$gconftool --get /desktop/gnome/url-handlers/mailto/command | cut -d ' ' -f 1"
            ]
        );
    }
    else {
        show_message_dialog(
            parent  => $parent,
            type    => 'error',
            buttons => 'close',
            text    => __(
'Unable to determine your desktop enviroment, and therefore your email client.'
            )
        );
        return;
    }

    if ( $client =~ /(thunderbird|icedove)/smx ) {
        return [ $1, '-compose', "attachment=file://$attachment" ];
    }
    return [ 'xdg-email', '--attach', $attachment, 'x@y' ];
}

# Display page selector and email.

sub email {

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

    $windowe = Gscan2pdf::Dialog::Save->new(
        'transient-for'  => $window,
        title            => __('Email as PDF'),
        'hide-on-delete' => TRUE,
        'page-range'     => $SETTING{'Page range'},
        'include-time'   => $SETTING{use_time},
        'meta-datetime'  => [
            Add_Delta_DHMS( Today_and_Now(), @{ $SETTING{'datetime offset'} } )
        ],

        # TRUE if any value is non-zero
        'select-datetime' =>
          scalar( grep { !/^0$/xsm } @{ $SETTING{'datetime offset'} } ),
        'meta-title'                => $SETTING{'title'},
        'meta-title-suggestions'    => $SETTING{'title-suggestions'},
        'meta-author'               => $SETTING{'author'},
        'meta-author-suggestions'   => $SETTING{'author-suggestions'},
        'meta-subject'              => $SETTING{'subject'},
        'meta-subject-suggestions'  => $SETTING{'subject-suggestions'},
        'meta-keywords'             => $SETTING{'keywords'},
        'meta-keywords-suggestions' => $SETTING{'keywords-suggestions'},
        'jpeg-quality'              => $SETTING{quality},
        'downsample-dpi'            => $SETTING{'downsample dpi'},
        downsample                  => $SETTING{downsample},
        'pdf-compression'           => $SETTING{'pdf compression'},
        'pdf-font'                  => $SETTING{'pdf font'},
        'can-encrypt-pdf'           => defined $dependencies{pdftk},
    );

    # Frame for page range
    $windowe->add_page_range;

    # Metadata
    $windowe->add_metadata;

    # PDF options
    my ( $vboxp, $hboxp ) = $windowe->add_pdf_options;

    $windowe->add_actions(
        'gtk-ok',
        sub {

            # Set options
            if ( not update_metadata_settings($windowe) ) {
                email();
                return;
            }

            # Compile list of pages
            $SETTING{'Page range'} = $windowe->get('page-range');
            my $list_of_pages = list_of_pages;

            # dig out the compression
            $SETTING{downsample}        = $windowe->get('downsample');
            $SETTING{'downsample dpi'}  = $windowe->get('downsample-dpi');
            $SETTING{'pdf compression'} = $windowe->get('pdf-compression');
            $SETTING{quality}           = $windowe->get('jpeg-quality');

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

            my $filename = Gscan2pdf::Document::expand_metadata_pattern(
                template => $SETTING{'default filename'},
                convert_whitespace =>
                  $SETTING{'convert whitespace to underscores'},
                author        => $SETTING{author},
                title         => $SETTING{title},
                docdate       => $windowe->get('meta-datetime'),
                today_and_now => [ Today_and_Now() ],
                extension     => 'pdf',
            );
            if ( $filename =~ /^\s+$/xsm ) { $filename = 'document' }
            $pdf = "$session/$filename.pdf";
            my $mua = mua( $windowe, $pdf );
            if ( not defined $mua or not @{$mua} ) { return }

            # Create the PDF
            my ( $signal, $pid );
            $pid = $slist->save_pdf(
                path          => $pdf,
                list_of_pages => $list_of_pages,
                metadata      => Gscan2pdf::Document::collate_metadata(
                    \%SETTING,
                    [ Today_and_Now() ],
                    [ Timezone() ]
                ),
                options         => \%options,
                queued_callback => sub {
                    return update_tpbar(@_);
                },
                started_callback => sub {
                    my ( $thread, $process, $completed, $total ) = @_;
                    $signal =
                      setup_tpbar( $thread, $process, $completed, $total,
                        $pid );
                    return TRUE if ( defined $signal );
                },
                running_callback => sub {
                    return update_tpbar(@_);
                },
                finished_callback => sub {
                    my ( $new_page, $pending ) = @_;
                    if ( not $pending ) { $thbox->hide }
                    if ( defined $signal ) {
                        $tcbutton->signal_handler_disconnect($signal);
                    }
                    mark_pages($list_of_pages);
                    if ( defined $SETTING{'view files toggle'}
                        and $SETTING{'view files toggle'} )
                    {
                        system "xdg-open \"$pdf\" &";
                    }
                    my $status = Gscan2pdf::Document::exec_command($mua);
                    if ($status) {
                        show_message_dialog(
                            parent  => $window,
                            type    => 'error',
                            buttons => 'close',
                            text    => __('Error creating email')
                        );
                    }
                },
                error_callback => \&error_callback
            );

            $windowe->hide;
        },
        'gtk-cancel',
        sub { $windowe->hide }
    );

    $windowe->show_all;
    return;
}

# Scan

sub scan_dialog {
    my ( $action, $hidden ) = @_;

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

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

    # scan pop-up window
    my %options = (
        'transient-for'       => $window,
        title                 => __('Scan Document'),
        'default-width'       => $SETTING{scan_window_width},
        'default-height'      => $SETTING{scan_window_height},
        logger                => $logger,
        dir                   => $session,
        'hide-on-delete'      => TRUE,
        'paper-formats'       => $SETTING{Paper},
        'allow-batch-flatbed' => $SETTING{'allow-batch-flatbed'},
        'adf-defaults-scan-all-pages' =>
          $SETTING{'adf-defaults-scan-all-pages'},
        'page-number-start' => $#{ $slist->{data} } > $EMPTY_LIST
        ? $slist->{data}[ $#{ $slist->{data} } ][0] + 1
        : 1,
    );
    if ( $SETTING{frontend} eq 'libimage-sane-perl' ) {
        $windows = Gscan2pdf::Dialog::Scan::Image_Sane->new(
            %options,
            'cycle-sane-handle'    => $SETTING{'cycle sane handle'},
            'cancel-between-pages' => (
                      $SETTING{'allow-batch-flatbed'}
                  and $SETTING{'cancel-between-pages'}
            ),
        );
    }
    else {
        $windows = Gscan2pdf::Dialog::Scan::CLI->new(
            %options,
            prefix                 => $SETTING{'scan prefix'},
            frontend               => $SETTING{'frontend'},
            'visible-scan-options' => $SETTING{'visible-scan-options'},
            'reload-triggers'      => $SETTING{'scan-reload-triggers'},
            'cache-options'        => $SETTING{'cache options'},
            'options-cache'        => $SETTING{cache},
        );
        $windows->signal_connect(
            'changed-options-cache' => sub {
                my ( $widget, $cache ) = @_;
                $SETTING{cache} = $cache;
            }
        );
    }

    # Can't set the device when creating the window,
    # as the list does not exist then
    $windows->signal_connect(
        'changed-device-list' => \&changed_device_list_callback );

    # Update default device
    $windows->signal_connect( 'changed-device' => \&changed_device_callback );

    # Check that there is room in the list for the number of pages
    $windows->signal_connect( 'changed-num-pages' => sub { update_number() } );
    $windows->signal_connect(
        'changed-page-number-start' => sub {
            my ( $widget, $value ) = @_;
            $windows->set(
                'max-pages',
                $slist->pages_possible(
                    $value, $windows->get('page-number-increment')
                )
            );
        }
    );
    $windows->signal_connect(
        'changed-page-number-increment' => sub {
            my ( $widget, $step ) = @_;
            $windows->set(
                'max-pages',
                $slist->pages_possible(
                    $windows->get('page-number-start'), $step
                )
            );
            update_postprocessing_options_callback();
        }
    );

    $windows->signal_connect(
        'changed-side-to-scan' => \&changed_side_to_scan_callback );

    my $signal;
    $windows->signal_connect(
        'started-process' => sub {
            my ( $widget, $message ) = @_;
            $logger->debug(
                "signal 'started-process' emitted with message: $message");
            $spbar->set_fraction(0);
            $spbar->set_text($message);
            $shbox->show_all;
            $signal = $scbutton->signal_connect(
                clicked => sub {
                    $windows->cancel_scan;
                }
            );
        }
    );

    $windows->signal_connect(
        'changed-progress' => \&changed_progress_callback );

    $windows->signal_connect(
        'finished-process' => sub {
            my ( $widget, $process, $button_signal ) = @_;
            $logger->debug(
                "signal 'finished-process' emitted with data: $process");
            if ( defined $button_signal ) {
                $scbutton->signal_handler_disconnect($button_signal);
            }
            $shbox->hide;
            if (    $process eq 'scan_pages'
                and $windows->get('sided') eq 'double' )
            {
                my ( $message, $next );
                if ( $windows->get('side-to-scan') eq 'facing' ) {
                    $message =
                      __('Finished scanning facing pages. Scan reverse pages?');
                    $next = 'reverse';
                }
                else {
                    $message =
                      __('Finished scanning reverse pages. Scan facing pages?');
                    $next = 'facing';
                }
                my $response = ask_question(
                    parent             => $windows,
                    type               => 'question',
                    buttons            => 'ok-cancel',
                    text               => $message,
                    'default-response' => 'ok',
                    'store-response'   => TRUE,
                    'stored-responses' => ['ok']
                );
                if ( $response eq 'ok' ) {
                    $windows->set( 'side-to-scan', $next );
                }
            }
        }
    );

    $windows->signal_connect(
        'process-error' => \&process_error_callback,
        $signal
    );

    # Profiles
    for my $profile ( keys %{ $SETTING{profile} } ) {
        $windows->add_profile(
            $profile,
            Gscan2pdf::Scanner::Profile->new_from_data(
                $SETTING{profile}{$profile}
            )
        );
    }
    $windows->signal_connect(
        'changed-profile' => sub {
            my ( $widget, $profile ) = @_;
            $SETTING{'default profile'} = $profile;
        }
    );
    $windows->signal_connect(
        'added-profile' => sub {
            my ( $widget, $name, $profile ) = @_;
            $SETTING{profile}{$name} = $profile->get_data;
        }
    );
    $windows->signal_connect(
        'removed-profile' => sub {
            my ( $widget, $profile ) = @_;
            delete $SETTING{profile}{$profile};
        }
    );

    # Update the default profile when the scan options change
    $windows->signal_connect(
        'changed-current-scan-options' => sub {
            my ( $widget, $profile ) = @_;
            $SETTING{'default-scan-options'} = $profile->get_data;
        }
    );

    $windows->signal_connect(
        'changed-paper-formats' => sub {
            my ( $widget, $formats ) = @_;
            $SETTING{Paper} = $formats;
        }
    );

    $windows->signal_connect( 'new-scan' => \&new_scan_callback );

    $windows->signal_connect(
        'changed-scan-option' => \&update_postprocessing_options_callback );

    add_postprocessing_options($windows);

    if ( not $hidden ) { $windows->show_all }

    update_postprocessing_options_callback();

    if (@device) {
        my @device_list;
        for (@device) {
            push @device_list, { name => $_, label => $_ };
        }
        $windows->set( 'device-list', \@device_list );
    }
    else {
        $windows->get_devices;
    }
    return;
}

sub changed_device_callback {
    my ( $widget, $device ) = @_;
    if ( defined $device and $device ne $EMPTY ) {
        $logger->info("signal 'changed-device' emitted with data: '$device'");
        $SETTING{device} = $device;
    }
    else {
        $logger->info("signal 'changed-device' emitted with data: undef");
    }

    # Can't set the profile until the options have been loaded. This
    # should only be called the first time after loading the available options
    $windows->{reloaded_signal} = $windows->signal_connect(
        'reloaded-scan-options' => \&reloaded_scan_options_callback );
    return;
}

sub changed_device_list_callback {
    my ( $widget, $device_list ) = @_;
    $logger->info( "signal 'changed-device-list' emitted with data: "
          . Dumper($device_list) );
    if ( defined $device_list and @{$device_list} ) {

        # Apply the device blacklist
        if ( defined $SETTING{'device blacklist'}
            and $SETTING{'device blacklist'} ne $EMPTY )
        {
            my @device_list = @{$device_list};
            my $i           = 0;
            while ( $i < @device_list ) {
                if ( $device_list[$i]{name} =~
                    /$SETTING{'device blacklist'}/xsm )
                {
                    $logger->info("Blacklisting device $device_list[$i]{name}");
                    splice @device_list, $i, 1;
                }
                else {
                    $i++;
                }
            }
            if ( @device_list < @{$device_list} ) {
                $windows->set( 'device-list', \@device_list );
                return;
            }
        }

       # Only set default device if it hasn't been specified on the command line
       # and it is in the the device list
        if ( defined $SETTING{device} and not @device ) {
            for ( @{$device_list} ) {
                if ( $SETTING{device} eq $_->{name} ) {
                    $windows->set( 'device', $SETTING{device} );
                    return;
                }
            }
        }
        $windows->set( 'device', $device_list->[0]{name} );
    }
    else {
        undef $windows;
    }
    return;
}

sub changed_side_to_scan_callback {
    my ( $widget, $side ) = @_;
    if ( $#{ $slist->{data} } > $EMPTY_LIST ) {
        $windows->set( 'page-number-start',
            $slist->{data}[ $#{ $slist->{data} } ][0] + 1 );
    }
    else {
        $windows->set( 'page-number-start', 1 );
    }
    return;
}

# This should only be called the first time after loading the available options
sub reloaded_scan_options_callback {
    $windows->signal_handler_disconnect( $windows->{reloaded_signal} );

    my @profiles = keys %{ $SETTING{profile} };
    if ( defined $SETTING{'default profile'} ) {
        $windows->set( 'profile', $SETTING{'default profile'} );
    }
    elsif ( defined $SETTING{'default-scan-options'} ) {
        $windows->set_current_scan_options(
            Gscan2pdf::Scanner::Profile->new_from_data(
                $SETTING{'default-scan-options'}
            )
        );
    }
    elsif (@profiles) {
        $windows->set( 'profile', $profiles[0] );
    }
    update_postprocessing_options_callback();
    return;
}

sub changed_progress_callback {
    my ( $widget, $progress, $message ) = @_;
    if ( defined $progress and $progress >= 0 and $progress <= 1 ) {
        $spbar->set_fraction($progress);
    }
    else {
        $spbar->pulse;
    }
    if ( defined $message ) { $spbar->set_text($message) }
    return;
}

sub new_scan_callback {
    my ( $widget, $path, $page_number, $xresolution, $yresolution ) = @_;

    # Update undo/redo buffers
    take_snapshot();

    my $rotate =
      $page_number % 2 ? $SETTING{'rotate facing'} : $SETTING{'rotate reverse'};
    my ( $signal, $pid );
    my %options = (
        page            => $page_number,
        dir             => $session,
        to_png          => $SETTING{to_png},
        rotate          => $rotate,
        ocr             => $SETTING{'OCR on scan'},
        engine          => $SETTING{'ocr engine'},
        language        => $SETTING{'ocr language'},
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            $slist->save_session;
        },
        error_callback => \&error_callback,
    );
    if ( $SETTING{'unpaper on scan'} ) {
        $options{unpaper} = $unpaper;
    }
    if ( $SETTING{'threshold-before-ocr'} ) {
        $options{threshold} = $SETTING{'threshold tool'};
    }
    if ( $SETTING{udt_on_scan} ) {
        $options{udt} = $SETTING{current_udt};
    }
    if ( defined $test_image ) {
        $options{filename} = $test_image;
        $options{delete}   = FALSE;
    }
    else {
        $logger->info(
            "Importing scan with resolution=$xresolution,$yresolution");
        $options{filename}    = $path;
        $options{xresolution} = $xresolution;
        $options{yresolution} = $yresolution;
        $options{delete}      = TRUE;
    }
    $slist->import_scan(%options);
    return;
}

sub process_error_callback {
    my ( $widget, $process, $msg, $signal ) = @_;
    $logger->info("signal 'process-error' emitted with data: $process $msg");
    if ( defined $signal ) {
        $scbutton->signal_handler_disconnect($signal);
    }
    $shbox->hide;

    # If we get an error opening a device,
    # remove it from the device list
    if ( $process =~ /^(?:get_devices|open_device|find_scan_options)$/xsm ) {
        my $device_list = $widget->get('device-list');
        my $device      = $widget->get('device');
        if ( defined $device ) {
            my @device_list;
            for ( @{$device_list} ) {
                if ( $_->{name} ne $device ) { push @device_list, $_ }
            }
            $widget->set( 'device-list', \@device_list );
        }
    }
    show_message_dialog(
        parent           => $widget,
        type             => 'error',
        buttons          => 'close',
        page             => $EMPTY,
        process          => $process,
        text             => $msg,
        'store-response' => TRUE
    );
    return;
}

sub update_postprocessing_options_callback {
    my $options   = $windows->get('available-scan-options');
    my $increment = $windows->get('page-number-increment');
    if ( defined $options ) {
        if ( $increment != 1 or $options->can_duplex ) {
            $rotate_side_cmbx->show;
            $rotate_side_cmbx2->show;
        }
        else {
            $rotate_side_cmbx->hide;
            $rotate_side_cmbx2->hide;
        }
    }
    return;
}

sub add_postprocessing_rotate {
    my ($vbox) = @_;
    my $hboxr = Gtk3::HBox->new;
    $vbox->pack_start( $hboxr, FALSE, FALSE, 0 );
    my $rbutton = Gtk3::CheckButton->new( __('Rotate') );
    $rbutton->set_tooltip_text( __('Rotate image after scanning') );
    $hboxr->pack_start( $rbutton, TRUE, TRUE, 0 );
    my @side = (
        [ 'both',    __('Both sides'),   __('Both sides.') ],
        [ 'facing',  __('Facing side'),  __('Facing side.') ],
        [ 'reverse', __('Reverse side'), __('Reverse side.') ],
    );
    $rotate_side_cmbx = Gscan2pdf::ComboBoxText->new_from_array(@side);
    $rotate_side_cmbx->set_tooltip_text( __('Select side to rotate') );
    $hboxr->pack_start( $rotate_side_cmbx, TRUE, TRUE, 0 );
    my @rotate = (
        [ $_90_DEGREES,  __('90'),  __('Rotate image 90 degrees clockwise.') ],
        [ $_180_DEGREES, __('180'), __('Rotate image 180 degrees clockwise.') ],
        [
            $_270_DEGREES, __('270'),
            __('Rotate image 90 degrees anticlockwise.')
        ],
    );
    my $comboboxr = Gscan2pdf::ComboBoxText->new_from_array(@rotate);
    $comboboxr->set_tooltip_text( __('Select direction of rotation') );
    $hboxr->pack_end( $comboboxr, TRUE, TRUE, 0 );

    $hboxr = Gtk3::HBox->new;
    $vbox->pack_start( $hboxr, FALSE, FALSE, 0 );
    my $r2button = Gtk3::CheckButton->new( __('Rotate') );
    $r2button->set_tooltip_text( __('Rotate image after scanning') );
    $hboxr->pack_start( $r2button, TRUE, TRUE, 0 );
    my @side2;
    $rotate_side_cmbx2 = Gtk3::ComboBoxText->new;
    $rotate_side_cmbx2->set_tooltip_text( __('Select side to rotate') );
    $hboxr->pack_start( $rotate_side_cmbx2, TRUE, TRUE, 0 );
    my $comboboxr2 = Gscan2pdf::ComboBoxText->new_from_array(@rotate);
    $comboboxr2->set_tooltip_text( __('Select direction of rotation') );
    $hboxr->pack_end( $comboboxr2, TRUE, TRUE, 0 );

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

                # Empty combobox
                while ( $rotate_side_cmbx2->get_active > $EMPTY_LIST ) {
                    $rotate_side_cmbx2->remove(0);
                    $rotate_side_cmbx2->set_active(0);
                }
                @side2 = ();
                for (@side) {
                    if (    $_->[0] ne 'both'
                        and $_->[0] ne
                        $side[ $rotate_side_cmbx->get_active ]->[0] )
                    {
                        push @side2, $_;
                    }
                }
                $rotate_side_cmbx2->append_text( $side2[0]->[1] );
                $rotate_side_cmbx2->set_active(0);
            }
        }
    );

    # In case it isn't set elsewhere
    $comboboxr2->set_active_index($_90_DEGREES);

    if ( $SETTING{'rotate facing'} or $SETTING{'rotate reverse'} ) {
        $rbutton->set_active(TRUE);
    }
    if ( $SETTING{'rotate facing'} == $SETTING{'rotate reverse'} ) {
        $rotate_side_cmbx->set_active_index('both');
        $comboboxr->set_active_index( $SETTING{'rotate facing'} );
    }
    elsif ( $SETTING{'rotate facing'} ) {
        $rotate_side_cmbx->set_active_index('facing');
        $comboboxr->set_active_index( $SETTING{'rotate facing'} );
        if ( $SETTING{'rotate reverse'} ) {
            $r2button->set_active(TRUE);
            $rotate_side_cmbx2->set_active_index('reverse');
            $comboboxr2->set_active_index( $SETTING{'rotate reverse'} );
        }
    }
    else {
        $rotate_side_cmbx->set_active_index('reverse');
        $comboboxr->set_active_index( $SETTING{'rotate reverse'} );
    }
    return ( \@rotate, \@side, \@side2, $rbutton, $r2button,
        $comboboxr, $comboboxr2 );
}

sub add_postprocessing_udt {
    my ($vboxp) = @_;
    my $hboxudt = Gtk3::HBox->new;
    $vboxp->pack_start( $hboxudt, FALSE, FALSE, 0 );
    my $udtbutton =
      Gtk3::CheckButton->new( __('Process with user-defined tool') );
    $udtbutton->set_tooltip_text(
        __('Process scanned images with user-defined tool') );
    $hboxudt->pack_start( $udtbutton, TRUE, TRUE, 0 );
    if ( not $SETTING{user_defined_tools} ) {
        $hboxudt->set_sensitive(FALSE);
        $udtbutton->set_active(FALSE);
    }
    elsif ( $SETTING{udf_on_scan} ) {
        $udtbutton->set_active(TRUE);
    }
    return $udtbutton, add_udt_combobox($hboxudt);
}

sub add_udt_combobox {
    my ($hbox) = @_;
    my @toolarray;
    for ( @{ $SETTING{user_defined_tools} } ) {
        push @toolarray, [ $_, $_ ];
    }
    my $combobox = Gscan2pdf::ComboBoxText->new_from_array(@toolarray);
    $combobox->set_active_index( $SETTING{current_udt} );
    $hbox->pack_start( $combobox, TRUE, TRUE, 0 );
    return $combobox;
}

sub add_postprocessing_ocr {
    my ($vbox) = @_;
    my $hboxo = Gtk3::HBox->new;
    $vbox->pack_start( $hboxo, FALSE, FALSE, 0 );
    my $obutton = Gtk3::CheckButton->new( __('OCR scanned pages') );
    $obutton->set_tooltip_text( __('OCR scanned pages') );
    if ( not $dependencies{ocr} ) {
        $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 = Gscan2pdf::ComboBoxText->new_from_array(@ocr_engine);
    $comboboxe->set_tooltip_text( __('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($vbox);
        $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;
                }
            }
        );
        if ( not $obutton->get_active ) { $hboxtl->set_sensitive(FALSE) }
        $obutton->signal_connect(
            toggled => sub {
                if ( $obutton->get_active ) {
                    $hboxtl->set_sensitive(TRUE);
                }
                else {
                    $hboxtl->set_sensitive(FALSE);
                }
            }
        );
    }
    if ( $dependencies{cuneiform} ) {
        ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vbox);
        $comboboxe->signal_connect(
            changed => sub {
                if ( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' )
                {
                    $hboxcl->show_all;
                }
                else {
                    $hboxcl->hide;
                }
            }
        );
    }
    $comboboxe->set_active_index( $SETTING{'ocr engine'} );

    my $cbto = Gtk3::CheckButton->new_with_label( __('Threshold before OCR') );
    $cbto->set_tooltip_text(
        __(
                'Threshold the image before performing OCR. '
              . 'This only affects the image passed to the OCR engine, and not the image stored.'
        )
    );
    $cbto->set_active( $SETTING{'threshold-before-ocr'} );
    $vbox->pack_start( $cbto, TRUE, TRUE, 0 );

    # SpinButton for threshold
    my $hboxt = Gtk3::HBox->new;
    $vbox->pack_start( $hboxt, FALSE, TRUE, 0 );
    my $label = Gtk3::Label->new( __('Threshold') );
    $hboxt->pack_start( $label, FALSE, TRUE, 0 );
    my $labelp = Gtk3::Label->new($PERCENT);
    $hboxt->pack_end( $labelp, FALSE, TRUE, 0 );
    my $spinbutton = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    $spinbutton->set_value( $SETTING{'threshold tool'} );
    $spinbutton->set_sensitive( $cbto->get_active ? TRUE : FALSE );
    $hboxt->pack_end( $spinbutton, FALSE, TRUE, 0 );
    $cbto->signal_connect(
        toggled => sub {
            $spinbutton->set_sensitive( $cbto->get_active ? TRUE : FALSE );
        }
    );

    return (
        $obutton,    $comboboxe, $hboxtl,  $comboboxtl, $hboxcl,
        $comboboxcl, \@tesslang, \@cflang, $cbto,       $spinbutton,
    );
}

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

    # pick up the vbox inside the scrolled window, not the main content area
    my $vbox = $self->{vbox};

    # Frame for post-processing
    my $framep = Gtk3::Frame->new( __('Post-processing') );
    $vbox->pack_start( $framep, FALSE, FALSE, 0 );
    my $vboxp = Gtk3::VBox->new;
    $vboxp->set_border_width( $self->style_get('content-area-border') );
    $framep->add($vboxp);

    # Rotate
    my ( $rotate, $side, $side2, $rbutton, $r2button, $comboboxr, $comboboxr2 )
      = add_postprocessing_rotate($vboxp);

    # CheckButton for unpaper
    my $hboxu = Gtk3::HBox->new;
    $vboxp->pack_start( $hboxu, FALSE, FALSE, 0 );
    my $ubutton = Gtk3::CheckButton->new( __('Clean up images') );
    $ubutton->set_tooltip_text( __('Clean up scanned images with unpaper') );
    $hboxu->pack_start( $ubutton, TRUE, TRUE, 0 );
    if ( not $dependencies{unpaper} ) {
        $ubutton->set_sensitive(FALSE);
        $ubutton->set_active(FALSE);
    }
    elsif ( $SETTING{'unpaper on scan'} ) {
        $ubutton->set_active(TRUE);
    }
    my $button = Gtk3::Button->new( __('Options') );
    $button->set_tooltip_text( __('Set unpaper options') );
    $hboxu->pack_end( $button, TRUE, TRUE, 0 );
    $button->signal_connect(
        clicked => sub {
            my $windowuo = Gscan2pdf::Dialog->new(
                'transient-for' => $window,
                title           => __('unpaper options'),
            );
            $unpaper->add_options( $windowuo->get_content_area );

            $windowuo->add_actions(
                'gtk-ok',
                sub {

                    # Update $SETTING
                    $SETTING{'unpaper options'} = $unpaper->get_options;
                    $windowuo->destroy;
                },
                'gtk-cancel',
                sub { $windowuo->destroy }
            );
            $windowuo->show_all;
        }
    );

    # CheckButton for user-defined tool
    ( my $udtbutton, $self->{comboboxudt} ) = add_postprocessing_udt($vboxp);

    my (
        $obutton,    $comboboxe, $hboxtl, $comboboxtl, $hboxcl,
        $comboboxcl, $tesslang,  $cflang, $tbutton,    $tsb
    ) = add_postprocessing_ocr($vboxp);

    $self->signal_connect(
        'clicked-scan-button' => sub {
            $SETTING{'rotate facing'}  = 0;
            $SETTING{'rotate reverse'} = 0;
            if ( $rbutton->get_active ) {
                if ( $rotate_side_cmbx->get_active_index eq 'both' ) {
                    $SETTING{'rotate facing'}  = $comboboxr->get_active_index;
                    $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
                }
                elsif ( $rotate_side_cmbx->get_active_index eq 'facing' ) {
                    $SETTING{'rotate facing'} = $comboboxr->get_active_index;
                }
                else {
                    $SETTING{'rotate reverse'} = $comboboxr->get_active_index;
                }
                if ( $r2button->get_active ) {
                    if ( $rotate_side_cmbx2->get_active_index eq 'facing' ) {
                        $SETTING{'rotate facing'} =
                          $comboboxr2->get_active_index;
                    }
                    else {
                        $SETTING{'rotate reverse'} =
                          $comboboxr2->get_active_index;
                    }
                }
            }
            $logger->info("rotate facing $SETTING{'rotate facing'}");
            $logger->info("rotate reverse $SETTING{'rotate reverse'}");

            $SETTING{'unpaper on scan'} = $ubutton->get_active;
            $logger->info("unpaper $SETTING{'unpaper on scan'}");

            $SETTING{udt_on_scan} = $udtbutton->get_active;
            $SETTING{current_udt} = $self->{comboboxudt}->get_active_text;
            $logger->info("UDT $SETTING{udt_on_scan}");
            if ( defined $SETTING{current_udt} ) {
                $logger->info("Current UDT $SETTING{current_udt}");
            }

            $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];
                if (   $SETTING{'ocr engine'} eq 'tesseract'
                    or $SETTING{'ocr engine'} eq 'ocropus' )
                {
                    $SETTING{'ocr language'} = $comboboxtl->get_active_index;
                }
                if ( $SETTING{'ocr engine'} eq 'cuneiform' ) {
                    $SETTING{'ocr language'} = $comboboxcl->get_active_index;
                }
                $SETTING{'threshold-before-ocr'} = $tbutton->get_active;
                $logger->info(
                    "threshold-before-ocr $SETTING{'threshold-before-ocr'}");
                $SETTING{'threshold tool'} = $tsb->get_value;
            }

        }
    );

    $self->signal_connect(
        show => sub {
            if (
                defined $hboxtl
                and
                not(   $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
                    or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
              )
            {
                $hboxtl->hide;
            }
            if (
                defined $hboxcl
                and
                not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' )
              )
            {
                $hboxcl->hide;
            }
        }
    );

    return;
}

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

sub update_start {
    if ( not defined $windows ) { return }
    my $value = $windows->get('page-number-start');
    if ( not defined $start ) { $start = $windows->get('page-number-start') }
    my $step = $value - $start;
    if ( $step == 0 ) { $step = $windows->get('page-number-increment') }
    my $exists = TRUE;
    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 : $EMPTY_LIST;
        }
    }
    $windows->set( 'page-number-start', $value );
    $start = $value;

    update_number();
    return;
}

# Update the number of pages to scan spinbutton if necessary

sub update_number {
    if ( not defined $windows ) { return }
    my $n = $slist->pages_possible(
        $windows->get('page-number-start'),
        $windows->get('page-number-increment')
    );
    if ( $n > 0 and $n < $windows->get('num-pages') ) {
        $windows->set( 'num-pages', $n );
    }
    return;
}

# print

sub print_dialog {
    chdir $SETTING{cwd};
    my $print_op = Gtk3::PrintOperation->new;

    if ( defined $print_settings ) {
        $print_op->set_print_settings($print_settings);
    }

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

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

    $print_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 = Gtk3::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;
            if ( $pheight / $iheight < $scale ) { $scale = $pheight / $iheight }
            $cr->scale( $scale, $scale );

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

            # Paint
            $cr->paint;

            return;
        }
    );

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

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

# Cut the selection

sub cut_selection {
    $clipboard = $slist->cut_selection;
    return;
}

# Copy the selection

sub copy_selection {
    $clipboard = $slist->copy_selection(TRUE);
    return;
}

# Paste the selection

sub paste_selection {
    if ( not defined $clipboard ) { return }
    my @pages = $slist->get_selected_indices;
    if (@pages) {
        $slist->paste_selection( $clipboard, $pages[-1], 'after', TRUE );
    }
    else {
        $slist->paste_selection( $clipboard, undef, undef, TRUE );
    }
    return;
}

# Delete the selected scans

sub delete_selection {

    # Update undo/redo buffers
    take_snapshot();

    $slist->delete_selection_extra;

    # Reset start page in scan dialog
    reset_start();
    return;
}

# Reset start page number after delete or new

sub reset_start {
    if ( defined $windows ) {
        if ( $#{ $slist->{data} } > $EMPTY_LIST ) {
            my $start_page = $windows->get('page-number-start');
            my $step       = $windows->get('page-number-increment');
            if ( $start_page >
                $slist->{data}[ $#{ $slist->{data} } ][0] + $step )
            {
                $windows->set( 'page-number-start',
                    $slist->{data}[ $#{ $slist->{data} } ][0] + $step );
            }
        }
        else {
            $windows->set( 'page-number-start', 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} } ) {
        if ( $slist->{data}[$_][0] % 2 xor $odd ) { push @selection, $_ }
    }

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

sub select_modified_since_ocr {
    my @selection;
    for 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;
        if ( $ocr_flag and ( $ocr_time le $dirty_time ) ) {
            push @selection, $_;
        }
    }

    $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} } ) {
        if ( not defined $slist->{data}[$_][2]{hocr} ) {
            push @selection, $_;
        }
    }

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

    # Clear the existing canvas
    $canvas->clear_text;

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

# Analyse and select blank pages

sub analyse_select_blank {
    analyse( 1, 0 );
    return;
}

# Select blank pages

sub select_blank_pages {
    for my $page ( 0 .. $#{ $slist->{data} } ) {

        #compare Std Dev to threshold
        if ( $slist->{data}[$page][2]{std_dev} <= $SETTING{'Blank threshold'} )
        {
            $slist->select($page);
            $logger->info('Selecting blank page');
        }
        else {
            $slist->unselect($page);
            $logger->info('Unselecting non-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 {
    for my $page ( 0 .. $#{ $slist->{data} } ) {

        #compare Mean to threshold
        if ( $slist->{data}[$page][2]{mean} <= $SETTING{'Dark threshold'} ) {
            $slist->select($page);
            $logger->info('Selecting dark page');
        }
        else {
            $slist->unselect($page);
            $logger->info('Unselecting non-dark page');
        }
        $logger->info( 'mean: '
              . $slist->{data}[$page][2]{mean}
              . ' threshold: '
              . $SETTING{'Dark threshold'} );
    }
    return;
}

# Display about dialog

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

    # Gtk3::AboutDialog->set_url_hook ($func, $data=undef);
    # Gtk3::AboutDialog->set_email_hook ($func, $data=undef);
    $about->set_program_name($prog_name);
    $about->set_version($VERSION);
    my $authors = [
        'Frederik Elwert',
        'Klaus Ethgen',
        'Andy Fingerhut',
        'Leon Fisk',
        'John Goerzen',
        'Alistair Grant',
        'David Hampton',
        'Sascha Hunold',
        'Jason Kankiewicz',
        'Matthijs Kooijman',
        'Peter Marschall',
        'Chris Mayo',
        'Hiroshi Miura',
        'Petr Písař',
        'Pablo Saratxaga',
        'Torsten Schönfeld',
        'Roy Shahbazian',
        'Jarl Stefansson',
        'Wikinaut',
        'Jakub Wilk'
    ];
    $about->set_authors( ['Jeff Ratcliffe'] );
    $about->add_credit_section( 'Patches gratefully received from', $authors );
    $about->set_comments( __('To aid the scan-to-PDF process') );
    $about->set_copyright( __('Copyright 2006--2019 Jeffrey Ratcliffe') );
    my $licence = <<'EOS';
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright 2006 -- 2019 Jeffrey Ratcliffe <jffry@posteo.net>

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 <https://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
Yuri Chornoivan
Davidmp
Whistle
Dusan Kazik
Cédric VALMARY (Tot en òc)
Eric Spierings
Milo Casagrande
Raúl González Duque
R120X
NSV
Alexandre Prokoudine
Aputsiaĸ Niels Janussen
Paul Wohlhart
Pierre Slamich
Tiago Silva
Igor Zubarev
Jarosław Ogrodnik
liorda
Clopy
Daniel Nylander
csola
Po-Hsu Lin
Tobias Bannert
Ettore Atalan
Eric Brandwein
Mikhail Novosyolov
rodroes
morodan
Hugues Drolet
Martin Butter
Albano Battistella
EOS
    $about->set_translator_credits($translators);
    $about->set_artists( ['lodp, Andreas E.'] );
    $about->set_logo(
        Gtk3::Gdk::Pixbuf->new_from_file("$iconpath/gscan2pdf.svg") );
    $about->set_transient_for($window);
    $about->run;
    $about->destroy;
    return;
}

# Dialog for renumber

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

    $windowrn = Gscan2pdf::Dialog::Renumber->new(
        'transient-for'  => $window,
        document         => $slist,
        logger           => $logger,
        'hide-on-delete' => FALSE,
    );

    # Update undo/redo buffers
    $windowrn->signal_connect(
        'before-renumber' => sub {
            take_snapshot();
        }
    );

    $windowrn->signal_connect(
        'error' => sub {
            my ($msg) = @_;
            show_message_dialog(
                parent  => $windowrn,
                type    => 'error',
                buttons => 'close',
                text    => $msg
            );
        }
    );

    $windowrn->show_all;
    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]->{uuid};
    }
    return @pages;
}

# Rotate selected images

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

    # Update undo/redo buffers
    take_snapshot();

    for my $page ( @{$pagelist} ) {
        my ( $signal, $pid );
        $pid = $slist->rotate(
            angle           => $angle,
            page            => $page,
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ($callback)      { $callback->($new_page) }
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                $slist->save_session;
            },
            error_callback   => \&error_callback,
            display_callback => sub {
                my ($new_page) = @_;
                display_image($new_page);
            },
        );
    }
    return;
}

# Analyse selected images

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

    # Update undo/redo buffers
    take_snapshot();

    my @pages_to_analyse;
    for 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"
            );
            push @pages_to_analyse, $slist->{data}[$i][2]->{uuid};
        }
    }
    if ( @pages_to_analyse > 0 ) {
        my ( $signal, $pid );
        $pid = $slist->analyse(
            list_of_pages   => \@pages_to_analyse,
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            running_callback => sub {
                return update_tpbar(@_);
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                if ($select_blank) { select_blank_pages() }
                if ($select_dark)  { select_dark_pages() }
                $slist->save_session;
            },
            error_callback => \&error_callback,
        );
    }
    else {
        if ($select_blank) { select_blank_pages() }
        if ($select_dark)  { select_dark_pages() }
    }
    return;
}

# Handle right-clicks

sub handle_clicks {
    my ( $widget, $event ) = @_;

    if ( $event->button == $RIGHT_MOUSE_BUTTON ) {
        if ( $widget->isa('Gscan2pdf::ImageView') ) {    # main image
            $uimanager->get_widget('/Detail_Popup')
              ->popup( undef, undef, undef, undef, $event->button,
                $event->time );
        }
        else {                                           # Thumbnail simplelist
            $SETTING{'Page range'} = 'selected';
            $uimanager->get_widget('/Thumb_Popup')
              ->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 {

    my $windowt = Gscan2pdf::Dialog->new(
        'transient-for' => $window,
        title           => __('Threshold'),
    );

    # Frame for page range
    $windowt->add_page_range;

    # SpinButton for threshold
    my $hboxt = Gtk3::HBox->new;
    my $vbox  = $windowt->get_content_area;
    $vbox->pack_start( $hboxt, FALSE, TRUE, 0 );
    my $label = Gtk3::Label->new( __('Threshold') );
    $hboxt->pack_start( $label, FALSE, TRUE, 0 );
    my $labelp = Gtk3::Label->new($PERCENT);
    $hboxt->pack_end( $labelp, FALSE, TRUE, 0 );
    my $spinbutton = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    $spinbutton->set_value( $SETTING{'threshold tool'} );
    $hboxt->pack_end( $spinbutton, FALSE, TRUE, 0 );

    # HBox for buttons
    $windowt->add_actions(
        'gtk-apply',
        sub {

            # Update undo/redo buffers
            take_snapshot();

            $SETTING{'threshold tool'} = $spinbutton->get_value;
            $SETTING{'Page range'}     = $windowt->get('page-range');

            my @pagelist =
              $slist->get_page_index( $SETTING{'Page range'},
                \&error_callback );
            if ( not @pagelist ) { return }
            my $page = 0;
            for my $i (@pagelist) {
                $page++;
                my ( $signal, $pid );
                $pid = $slist->threshold(
                    threshold       => $SETTING{'threshold tool'},
                    page            => $slist->{data}[$i][2]->{uuid},
                    queued_callback => sub {
                        return update_tpbar(@_);
                    },
                    started_callback => sub {
                        my ( $thread, $process, $completed, $total ) = @_;
                        $signal =
                          setup_tpbar( $thread, $process, $completed, $total,
                            $pid );
                        return TRUE if ( defined $signal );
                    },
                    finished_callback => sub {
                        my ( $new_page, $pending ) = @_;
                        if ( not $pending ) { $thbox->hide }
                        if ( defined $signal ) {
                            $tcbutton->signal_handler_disconnect($signal);
                        }
                        $slist->save_session;
                    },
                    error_callback   => \&error_callback,
                    display_callback => sub {
                        my ($new_page) = @_;
                        display_image($new_page);
                    },
                );
            }
        },
        'gtk-cancel',
        sub { $windowt->destroy }
    );
    $windowt->show_all;
    return;
}

# Display page selector and on apply brightness & contrast accordingly

sub brightness_contrast {

    my $windowt = Gscan2pdf::Dialog->new(
        'transient-for' => $window,
        title           => __('Brightness / Contrast'),
    );
    my ( $hbox, $label );

    # Frame for page range
    $windowt->add_page_range;

    # SpinButton for brightness
    $hbox = Gtk3::HBox->new;
    my $vbox = $windowt->get_content_area;
    $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
    $label = Gtk3::Label->new( __('Brightness') );
    $hbox->pack_start( $label, FALSE, TRUE, 0 );
    $label = Gtk3::Label->new($PERCENT);
    $hbox->pack_end( $label, FALSE, TRUE, 0 );
    my $spinbuttonb = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    $spinbuttonb->set_value( $SETTING{'brightness tool'} );
    $hbox->pack_end( $spinbuttonb, FALSE, TRUE, 0 );

    # SpinButton for contrast
    $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
    $label = Gtk3::Label->new( __('Contrast') );
    $hbox->pack_start( $label, FALSE, TRUE, 0 );
    $label = Gtk3::Label->new($PERCENT);
    $hbox->pack_end( $label, FALSE, TRUE, 0 );
    my $spinbuttonc = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    $spinbuttonc->set_value( $SETTING{'contrast tool'} );
    $hbox->pack_end( $spinbuttonc, FALSE, TRUE, 0 );

    # HBox for buttons
    $windowt->add_actions(
        'gtk-apply',
        sub {

            # Update undo/redo buffers
            take_snapshot();

            $SETTING{'brightness tool'} = $spinbuttonb->get_value;
            $SETTING{'contrast tool'}   = $spinbuttonc->get_value;
            $SETTING{'Page range'}      = $windowt->get('page-range');

            my @pagelist =
              $slist->get_page_index( $SETTING{'Page range'},
                \&error_callback );
            if ( not @pagelist ) { return }
            for my $i (@pagelist) {
                my ( $signal, $pid );
                $pid = $slist->brightness_contrast(
                    brightness      => $SETTING{'brightness tool'},
                    contrast        => $SETTING{'contrast tool'},
                    page            => $slist->{data}[$i][2]->{uuid},
                    queued_callback => sub {
                        return update_tpbar(@_);
                    },
                    started_callback => sub {
                        my ( $thread, $process, $completed, $total ) = @_;
                        $signal =
                          setup_tpbar( $thread, $process, $completed, $total,
                            $pid );
                        return TRUE if ( defined $signal );
                    },
                    finished_callback => sub {
                        my ( $new_page, $pending ) = @_;
                        if ( not $pending ) { $thbox->hide }
                        if ( defined $signal ) {
                            $tcbutton->signal_handler_disconnect($signal);
                        }
                        $slist->save_session;
                    },
                    error_callback   => \&error_callback,
                    display_callback => sub {
                        my ($new_page) = @_;
                        display_image($new_page);
                    },
                );
            }
        },
        'gtk-cancel',
        sub { $windowt->destroy }
    );
    $windowt->show_all;
    return;
}

# Display page selector and on apply negate accordingly

sub negate {

    my $windowt = Gscan2pdf::Dialog->new(
        'transient-for' => $window,
        title           => __('Negate'),
    );

    # Frame for page range
    $windowt->add_page_range;

    # HBox for buttons
    $windowt->add_actions(
        'gtk-apply',
        sub {

            # Update undo/redo buffers
            take_snapshot();

            $SETTING{'Page range'} = $windowt->get('page-range');
            my @pagelist =
              $slist->get_page_index( $SETTING{'Page range'},
                \&error_callback );
            if ( not @pagelist ) { return }
            for my $i (@pagelist) {
                my ( $signal, $pid );
                $pid = $slist->negate(
                    page            => $slist->{data}[$i][2]->{uuid},
                    queued_callback => sub {
                        return update_tpbar(@_);
                    },
                    started_callback => sub {
                        my ( $thread, $process, $completed, $total ) = @_;
                        $signal =
                          setup_tpbar( $thread, $process, $completed, $total,
                            $pid );
                        return TRUE if ( defined $signal );
                    },
                    finished_callback => sub {
                        my ( $new_page, $pending ) = @_;
                        if ( not $pending ) { $thbox->hide }
                        if ( defined $signal ) {
                            $tcbutton->signal_handler_disconnect($signal);
                        }
                        $slist->save_session;
                    },
                    error_callback   => \&error_callback,
                    display_callback => sub {
                        my ($new_page) = @_;
                        display_image($new_page);
                    },
                );
            }
        },
        'gtk-cancel',
        sub { $windowt->destroy }
    );
    $windowt->show_all;
    return;
}

# Display page selector and on apply unsharp accordingly

sub unsharp {

    my $windowum = Gscan2pdf::Dialog->new(
        'transient-for' => $window,
        title           => __('Unsharp mask'),
    );

    # Frame for page range
    $windowum->add_page_range;

    my $spinbuttonr = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    my $spinbuttons =
      Gtk3::SpinButton->new_with_range( 0, $MAX_SIGMA, $SIGMA_STEP );
    my $spinbuttong = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    my $spinbuttont =
      Gtk3::SpinButton->new_with_range( 0, 1, $UNIT_SLIDER_STEP );
    my @layout = (
        [
            __('Radius'),
            $spinbuttonr,
            __('pixels'),
            $SETTING{'unsharp radius'},
            __(
'The radius of the Gaussian, in pixels, not counting the center pixel (0 = automatic).'
            ),
        ],
        [
            __('Sigma'), $spinbuttons, __('pixels'),
            $SETTING{'unsharp sigma'},
            __('The standard deviation of the Gaussian.'),
        ],
        [
            __('Gain'),
            $spinbuttong,
            $PERCENT,
            $SETTING{'unsharp gain'},
            __(
'The percentage of the difference between the original and the blur image that is added back into the original.'
            ),
        ],
        [
            __('Threshold'),
            $spinbuttont,
            undef,
            $SETTING{'unsharp threshold'},
            __(
'The threshold, as a fraction of QuantumRange, needed to apply the difference amount.'
            ),
        ],
    );

    # grid for layout
    my $grid = Gtk3::Grid->new;
    my $vbox = $windowum->get_content_area;
    $vbox->pack_start( $grid, TRUE, TRUE, 0 );

    for my $row ( 0 .. $#layout ) {
        my $col   = 0;
        my $hbox  = Gtk3::HBox->new;
        my $label = Gtk3::Label->new( $layout[$row][$col] );
        $grid->attach( $hbox, $col++, $row, 1, 1 );
        $hbox->pack_start( $label, FALSE, TRUE, 0 );
        $hbox = Gtk3::HBox->new;
        $hbox->pack_end( $layout[$row][$col], TRUE, TRUE, 0 );
        $grid->attach( $hbox, $col, $row, 1, 1 );

        if ( defined $layout[$row][ ++$col ] ) {
            $hbox = Gtk3::HBox->new;
            $grid->attach( $hbox, $col, $row, 1, 1 );
            $label = Gtk3::Label->new( $layout[$row][$col] );
            $hbox->pack_start( $label, FALSE, TRUE, 0 );
        }
        if ( defined $layout[$row][ ++$col ] ) {
            $layout[$row][1]->set_value( $layout[$row][$col] );
        }
        $layout[$row][1]->set_tooltip_text( $layout[$row][ ++$col ] );
    }

    # HBox for buttons
    $windowum->add_actions(
        'gtk-apply',
        sub {

            # Update undo/redo buffers
            take_snapshot();

            $SETTING{'unsharp radius'}    = $spinbuttonr->get_value;
            $SETTING{'unsharp sigma'}     = $spinbuttons->get_value;
            $SETTING{'unsharp gain'}      = $spinbuttong->get_value;
            $SETTING{'unsharp threshold'} = $spinbuttont->get_value;
            $SETTING{'Page range'}        = $windowum->get('page-range');

            my @pagelist =
              $slist->get_page_index( $SETTING{'Page range'},
                \&error_callback );
            if ( not @pagelist ) { return }
            for my $i (@pagelist) {
                my ( $signal, $pid );
                $pid = $slist->unsharp(
                    page            => $slist->{data}[$i][2]->{uuid},
                    radius          => $SETTING{'unsharp radius'},
                    sigma           => $SETTING{'unsharp sigma'},
                    gain            => $SETTING{'unsharp gain'},
                    threshold       => $SETTING{'unsharp threshold'},
                    queued_callback => sub {
                        return update_tpbar(@_);
                    },
                    started_callback => sub {
                        my ( $thread, $process, $completed, $total ) = @_;
                        $signal =
                          setup_tpbar( $thread, $process, $completed, $total,
                            $pid );
                        return TRUE if ( defined $signal );
                    },
                    finished_callback => sub {
                        my ( $new_page, $pending ) = @_;
                        if ( not $pending ) { $thbox->hide }
                        if ( defined $signal ) {
                            $tcbutton->signal_handler_disconnect($signal);
                        }
                        $slist->save_session;
                    },
                    error_callback   => \&error_callback,
                    display_callback => sub {
                        my ($new_page) = @_;
                        display_image($new_page);
                    },
                );
            }
        },
        'gtk-cancel',
        sub { $windowum->destroy }
    );
    $windowum->show_all;
    return;
}

# Callback to switch between tabbed and split views

sub change_view_cb {
    my ( $action, $current ) = @_;
    $SETTING{viewer_tools} = $current->get_current_value();
    if ( $SETTING{viewer_tools} == $TABBED_VIEW ) {
        $hpaned->remove($hpanei);
        $hpanei->remove($view);
        $hpanei->remove($canvas);
    }
    else {    # $SPLIT_VIEW
        $hpaned->remove($vnotebook);
        $vnotebook->remove($view);
        $vnotebook->remove($canvas);
    }
    pack_viewer_tools();
    return;
}

# Display page selector and on apply crop accordingly

sub crop_dialog {
    my ($action) = @_;
    if ( defined $windowc ) {
        $windowc->present;
        return;
    }

    $windowc = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('Crop'),
        'hide-on-delete' => TRUE,
    );

    # Frame for page range
    $windowc->add_page_range;

    $sb_selector_x =
      Gtk3::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
    $sb_selector_y =
      Gtk3::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
    $sb_selector_w =
      Gtk3::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
    $sb_selector_h =
      Gtk3::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
    my @layout = (
        [
            __('x'),
            $sb_selector_x,
            __('The x-position of the left hand edge of the crop.'),
        ],
        [
            __('y'), $sb_selector_y,
            __('The y-position of the top edge of the crop.'),
        ],
        [ __('Width'),  $sb_selector_w, __('The width of the crop.'), ],
        [ __('Height'), $sb_selector_h, __('The height of the crop.'), ],
    );

    # grid for layout
    my $grid = Gtk3::Grid->new;
    my $vbox = $windowc->get_content_area;
    $vbox->pack_start( $grid, TRUE, TRUE, 0 );

    for my $row ( 0 .. $#layout ) {
        my $col   = 0;
        my $hbox  = Gtk3::HBox->new;
        my $label = Gtk3::Label->new( $layout[$row][$col] );
        $grid->attach( $hbox, $col++, $row, 1, 1 );
        $hbox->pack_start( $label, FALSE, TRUE, 0 );
        $hbox = Gtk3::HBox->new;
        $hbox->pack_end( $layout[$row][$col], TRUE, TRUE, 0 );
        $grid->attach( $hbox, $col, $row, 1, 1 );
        $hbox = Gtk3::HBox->new;
        $grid->attach( $hbox, ++$col, $row, 1, 1 );
        $label = Gtk3::Label->new( __('pixels') );
        $hbox->pack_start( $label, FALSE, TRUE, 0 );
        $layout[$row][1]->set_tooltip_text( $layout[$row][$col] );
    }

    # Callbacks if the spinbuttons change
    $sb_selector_x->signal_connect(
        'value-changed' => sub {
            $SETTING{selection}{x} = $sb_selector_x->get_value;
            $sb_selector_w->set_range( 0,
                $current_page->{w} - $SETTING{selection}{x} );
            update_selector();
        }
    );
    $sb_selector_y->signal_connect(
        'value-changed' => sub {
            $SETTING{selection}{y} = $sb_selector_y->get_value;
            $sb_selector_h->set_range( 0,
                $current_page->{h} - $SETTING{selection}{y} );
            update_selector();
        }
    );
    $sb_selector_w->signal_connect(
        'value-changed' => sub {
            $SETTING{selection}{width} = $sb_selector_w->get_value;
            $sb_selector_x->set_range( 0,
                $current_page->{w} - $SETTING{selection}{width} );
            update_selector();
        }
    );
    $sb_selector_h->signal_connect(
        'value-changed' => sub {
            $SETTING{selection}{height} = $sb_selector_h->get_value;
            $sb_selector_y->set_range( 0,
                $current_page->{h} - $SETTING{selection}{height} );
            update_selector();
        }
    );

    if ( defined $SETTING{selection}{x} ) {
        $sb_selector_x->set_value( $SETTING{selection}{x} );
    }
    if ( defined $SETTING{selection}{y} ) {
        $sb_selector_y->set_value( $SETTING{selection}{y} );
    }
    if ( defined $SETTING{selection}{width} ) {
        $sb_selector_w->set_value( $SETTING{selection}{width} );
    }
    if ( defined $SETTING{selection}{height} ) {
        $sb_selector_h->set_value( $SETTING{selection}{height} );
    }

    $windowc->add_actions(
        'gtk-apply',
        sub {
            $SETTING{'Page range'} = $windowc->get('page-range');
            crop_selection(
                $action,
                $slist->get_page_index(
                    $SETTING{'Page range'}, \&error_callback
                )
            );
        },
        'gtk-cancel',
        sub { $windowc->hide }
    );
    $windowc->show_all;
    return;
}

sub update_selector {
    my $sel = $view->get_selection;
    $view->signal_handler_block( $view->{selection_changed_signal} );
    if ( defined $sel ) {
        $view->set_selection( $SETTING{selection} );
    }
    $view->signal_handler_unblock( $view->{selection_changed_signal} );
    return;
}

sub crop_selection {
    my ( $action, @pagelist ) = @_;
    if ( not %{ $SETTING{selection} } ) { return }

    # Update undo/redo buffers
    take_snapshot();

    if ( not @pagelist or not defined $pagelist[0] ) {
        @pagelist = $slist->get_selected_indices;
    }
    if ( not @pagelist ) { return }
    for my $i (@pagelist) {
        my ( $signal, $pid );
        $pid = $slist->crop(
            page            => $slist->{data}[$i][2]->{uuid},
            x               => $SETTING{selection}{x},
            y               => $SETTING{selection}{y},
            w               => $SETTING{selection}{width},
            h               => $SETTING{selection}{height},
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                $slist->save_session;
            },
            error_callback   => \&error_callback,
            display_callback => sub {
                my ($new_page) = @_;
                display_image($new_page);
            },
        );
    }
    return;
}

sub user_defined_dialog {

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

    $windowudt = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('User-defined tools'),
        'hide-on-delete' => TRUE,
    );

    # Frame for page range
    $windowudt->add_page_range;

    my $hbox = Gtk3::HBox->new;
    my $vbox = $windowudt->get_content_area;
    $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
    my $label = Gtk3::Label->new( __('Selected tool') );
    $hbox->pack_start( $label, FALSE, TRUE, 0 );
    $comboboxudt = add_udt_combobox($hbox);

    $windowudt->add_actions(
        'gtk-ok',
        sub {
            $SETTING{'Page range'} = $windowudt->get('page-range');
            my @pagelist = indices2pages(
                $slist->get_page_index(
                    $SETTING{'Page range'}, \&error_callback
                )
            );
            if ( not @pagelist ) { return }
            $SETTING{current_udt} = $comboboxudt->get_active_text;
            user_defined_tool( \@pagelist, $SETTING{current_udt} );

            $windowudt->hide;
        },
        'gtk-cancel',
        sub { $windowudt->hide; }
    );

    $windowudt->show_all;
    return;
}

# Run a user-defined tool on the selected images

sub user_defined_tool {
    my ( $pages, $cmd ) = @_;

    # Update undo/redo buffers
    take_snapshot();

    for my $page ( @{$pages} ) {
        my ( $signal, $pid );
        $pid = $slist->user_defined(
            page            => $page,
            command         => $cmd,
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                $slist->save_session;
            },
            error_callback   => \&error_callback,
            display_callback => sub {
                my ($new_page) = @_;
                display_image($new_page);
            },
        );
    }
    return;
}

# queue $page to be processed by unpaper

sub unpaper_page {
    my ( $pages, $options, $callback ) = @_;
    if ( not defined $options ) { $options = $EMPTY }

    # Update undo/redo buffers
    take_snapshot();

    for my $pageobject ( @{$pages} ) {
        my ( $signal, $pid );
        $pid = $slist->unpaper(
            page            => $pageobject,
            options         => $options,
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
                $slist->save_session;
            },
            error_callback   => \&error_callback,
            display_callback => sub {
                my ($new_page) = @_;
                display_image($new_page);
                if ($callback) { $callback->($new_page) }
            },
        );
    }

    return;
}

# Run unpaper to clean up scan.

sub unpaper {

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

    $windowu = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('unpaper'),
        'hide-on-delete' => TRUE,
    );

    # Frame for page range
    $windowu->add_page_range;

    # add unpaper options
    my $vbox = $windowu->get_content_area;
    $unpaper->add_options($vbox);

    $windowu->add_actions(
        'gtk-ok',
        sub {

            # Update $SETTING
            $SETTING{'unpaper options'} = $unpaper->get_options;
            $SETTING{'Page range'}      = $windowu->get('page-range');

            # run unpaper
            my @pagelist = indices2pages(
                $slist->get_page_index(
                    $SETTING{'Page range'}, \&error_callback
                )
            );
            if ( not @pagelist ) { return }
            unpaper_page(
                \@pagelist,
                {
                    command   => $unpaper->get_cmdline,
                    direction => $unpaper->get_option('direction')
                }
            );

            $windowu->hide;
        },
        'gtk-cancel',
        sub { $windowu->hide; }
    );

    $windowu->show_all;
    return;
}

# Add hbox for tesseract languages

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

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

    # Tesseract language files
    my @tesslang;
    for ( sort { $a cmp $b } keys %{ Gscan2pdf::Tesseract->languages } ) {
        push @tesslang, [ $_, __( ${ Gscan2pdf::Tesseract->languages }{$_} ) ];
    }

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

    my $combobox = Gscan2pdf::ComboBoxText->new_from_array(@tesslang);
    $combobox->set_active_index( $SETTING{'ocr language'} );
    $hbox->pack_end( $combobox, FALSE, TRUE, 0 );
    return $hbox, $combobox, @tesslang;
}

# Add hbox for cuneiform languages

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

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

    # Tesseract language files
    my @lang;
    for ( sort { $a cmp $b } keys %{ Gscan2pdf::Cuneiform->languages } ) {
        push @lang, [ $_, __( ${ Gscan2pdf::Cuneiform->languages }{$_} ) ];
    }

    my $combobox = Gscan2pdf::ComboBoxText->new_from_array(@lang);
    $combobox->set_active_index( $SETTING{'ocr language'} );
    $hbox->pack_end( $combobox, FALSE, TRUE, 0 );
    return $hbox, $combobox, @lang;
}

# Run OCR on current page and display result

sub ocr_dialog {

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

    $windowo = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('OCR'),
        'hide-on-delete' => TRUE,
    );

    # Frame for page range
    $windowo->add_page_range;

    # OCR engine selection
    my $hboxe = Gtk3::HBox->new;
    my $vbox  = $windowo->get_content_area;
    $vbox->pack_start( $hboxe, FALSE, TRUE, 0 );
    my $label = Gtk3::Label->new( __('OCR Engine') );
    $hboxe->pack_start( $label, FALSE, TRUE, 0 );
    my $combobe = Gscan2pdf::ComboBoxText->new_from_array(@ocr_engine);
    $combobe->set_active_index( $SETTING{'ocr engine'} );
    $hboxe->pack_end( $combobe, FALSE, TRUE, 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;
                }
            }
        );
    }
    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;
                }
            }
        );
    }

    my $cbto = Gtk3::CheckButton->new_with_label( __('Threshold before OCR') );
    $cbto->set_tooltip_text(
        __(
                'Threshold the image before performing OCR. '
              . 'This only affects the image passed to the OCR engine, and not the image stored.'
        )
    );
    if ( defined $SETTING{'threshold-before-ocr'} ) {
        $cbto->set_active( $SETTING{'threshold-before-ocr'} );
    }
    $vbox->pack_start( $cbto, TRUE, TRUE, 0 );

    # SpinButton for threshold
    my $hboxt = Gtk3::HBox->new;
    $hboxt->set_sensitive( $cbto->get_active ? TRUE : FALSE );
    $vbox->pack_start( $hboxt, FALSE, TRUE, 0 );
    $label = Gtk3::Label->new( __('Threshold') );
    $hboxt->pack_start( $label, FALSE, TRUE, 0 );
    my $labelp = Gtk3::Label->new($PERCENT);
    $hboxt->pack_end( $labelp, FALSE, TRUE, 0 );
    my $spinbutton = Gtk3::SpinButton->new_with_range( 0, $_100_PERCENT, 1 );
    $spinbutton->set_value( $SETTING{'threshold tool'} );
    $hboxt->pack_end( $spinbutton, FALSE, TRUE, 0 );
    $cbto->signal_connect(
        toggled => sub {
            $hboxt->set_sensitive( $cbto->get_active ? TRUE : FALSE );
        }
    );

    $windowo->add_actions(
        __('Start OCR'),
        sub {
            my ( $tesslang, $cflang );
            if ( defined $comboboxtl ) {
                $tesslang = $tesslang[ $comboboxtl->get_active ]->[0];
            }
            if ( defined $comboboxcl ) {
                $cflang = $cflang[ $comboboxcl->get_active ]->[0];
            }
            run_ocr( $ocr_engine[ $combobe->get_active ]->[0],
                $tesslang, $cflang, $cbto->get_active, $spinbutton->get_value );
        },
        'gtk-close',
        sub { $windowo->hide }
    );

    $windowo->show_all;
    if (
        defined $hboxtl
        and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
            or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
      )
    {
        $hboxtl->hide;
    }
    if ( defined $hboxcl
        and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) )
    {
        $hboxcl->hide;
    }
    return;
}

sub run_ocr {
    my ( $engine, $tesslang, $cflang, $threshold_flag, $threshold ) = @_;
    if ( $engine eq 'tesseract' or $engine eq 'ocropus' ) {
        $SETTING{'ocr language'} = $tesslang;
    }
    if ( $SETTING{'ocr engine'} eq 'cuneiform' ) {
        $SETTING{'ocr language'} = $cflang;
    }
    my ( $signal, $pid );
    my %options = (
        queued_callback => sub {
            return update_tpbar(@_);
        },
        started_callback => sub {
            my ( $thread, $process, $completed, $total ) = @_;
            $signal =
              setup_tpbar( $thread, $process, $completed, $total, $pid );
            return TRUE if ( defined $signal );
        },
        finished_callback => sub {
            my ( $new_page, $pending ) = @_;
            if ( not $pending ) { $thbox->hide }
            if ( defined $signal ) {
                $tcbutton->signal_handler_disconnect($signal);
            }
            $slist->save_session;
        },
        error_callback   => \&error_callback,
        display_callback => sub {
            my ($new_page) = @_;
            my @page = $slist->get_selected_indices;
            if ( @page and $new_page == $slist->{data}[ $page[0] ][2] ) {
                create_canvas($new_page);
            }
        },
        engine   => $engine,
        language => $SETTING{'ocr language'},
    );
    $SETTING{'ocr engine'}           = $engine;
    $SETTING{'threshold-before-ocr'} = $threshold_flag;
    if ($threshold_flag) {
        $SETTING{'threshold tool'} = $threshold;
        $options{threshold} = $threshold;
    }

    # fill $pagelist with filenames
    # depending on which radiobutton is active
    $SETTING{'Page range'} = $windowo->get('page-range');
    my @pagelist = indices2pages(
        $slist->get_page_index( $SETTING{'Page range'}, \&error_callback ) );
    if ( not @pagelist ) { return }
    $slist->ocr_pages( \@pagelist, %options );
    $windowo->hide;
    return;
}

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

sub quit {

    if (
        not scans_saved(
            __("Some pages have not been saved.\nDo you really want to quit?")
        )
      )
    {
        return;
    }

    # 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;
    if ( defined $windows ) {
        ( $SETTING{scan_window_width}, $SETTING{scan_window_height} ) =
          $windows->get_size;
    }

    # Write config file
    Gscan2pdf::Config::write_config( $rc, $logger, \%SETTING );

    $logger->info('Killing Sane thread(s)');
    Gscan2pdf::Frontend::Image_Sane->quit();
    $logger->info('Killing document thread(s)');
    Gscan2pdf::Document->quit();
    $logger->debug('Quitting');
    return TRUE;
}

# Perhaps we should use gtk and mallard for this in the future

sub view_html {

    # At the moment, we have no translations,
    # but when we do, replace C with $locale
    my $html = "/usr/share/help/C/$prog_name/documentation.html";
    if ( not -e $html ) {
        $html = 'http://gscan2pdf.sf.net';
    }
    $logger->info("Opening $html in default webbrowser via xdg-open");
    system "xdg-open $html";
    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);

    # Check free space in $session directory
    my $df = df( "$session", $_1MB );
    if ( defined $df ) {
        $logger->debug(
"Free space in $session (Mb): $df->{bavail} (warning at $SETTING{'available-tmp-warning'})"
        );
        if ( $df->{bavail} < $SETTING{'available-tmp-warning'} ) {
            my $text = sprintf __('%dMb free in %s.'), $df->{bavail}, $session;
            show_message_dialog(
                parent  => $window,
                type    => 'warning',
                buttons => 'close',
                text    => $text
            );
        }

    }
    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->get_selection->signal_handler_block(
        $slist->{selection_changed_signal} );
    @{ $slist->{data} } = @undo_buffer;

    # Unblock slist signals now finished
    $slist->get_selection->signal_handler_unblock(
        $slist->{selection_changed_signal} );
    $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 = Gtk3::IconFactory->new();
    $iconfactory->add_default();

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

# Add icons

sub register_icon {
    my ( $stock_id, $path ) = @_;

    if ( not defined $iconfactory ) { return }

    my $icon;
    try { $icon = Gtk3::Gdk::Pixbuf->new_from_file($path) }
    catch { $logger->warn("Unable to load icon `$path': $_") };
    if ( defined $icon ) {
        $iconfactory->add( $stock_id, Gtk3::IconSet->new_from_pixbuf($icon) );
    }
    return;
}

# marked page list as saved

sub mark_pages {
    my ($pages) = @_;
    $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
    for ( @{$pages} ) {
        my $i = $slist->find_page_by_uuid($_);
        if ( defined $i ) {
            $slist->{data}[$i][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 (
        ask_question(
            parent  => $window,
            type    => 'question',
            buttons => 'ok-cancel',
            text    => __('This operation cannot be undone. Are you sure?'),
            'store-response'   => TRUE,
            'stored-responses' => ['ok']
        ) 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, $pid );
        $pid = $slist->to_png(
            page            => $_->[2],
            queued_callback => sub {
                return update_tpbar(@_);
            },
            started_callback => sub {
                my ( $thread, $process, $completed, $total ) = @_;
                $signal =
                  setup_tpbar( $thread, $process, $completed, $total, $pid );
                return TRUE if ( defined $signal );
            },
            finished_callback => sub {
                my ( $new_page, $pending ) = @_;
                if ( not $pending ) { $thbox->hide }
                if ( defined $signal ) {
                    $tcbutton->signal_handler_disconnect($signal);
                }
            },
            error_callback => \&error_callback,
        );
    }
    return;
}

# Expand tildes in the filename

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

# Preferences dialog

sub preferences {

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

    $windowr = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('Preferences'),
        'hide-on-delete' => TRUE,
    );
    my $vbox = $windowr->get_content_area;

    # Notebook for scan and general options
    my $notebook = Gtk3::Notebook->new;
    $vbox->pack_start( $notebook, TRUE, TRUE, 0 );

    my ( $vbox1, $frontends, $combob, $preentry, $cbc, $cbo,
        $blacklist, $cbcsh, $cb_batch_flatbed, $cb_cancel_btw_pages )
      = _preferences_scan_options( $windowr->style_get('content-area-border') );
    $notebook->append_page( $vbox1, Gtk3::Label->new( __('Scan options') ) );

    my (
        $vbox2,       $fileentry,   $cbw,         $cbtz,
        $cbtm,        $cbts,        $cbtp,        $tmpentry,
        $spinbuttonw, $spinbuttonb, $spinbuttond, $ocr_function,
        $comboo,      $cbv,         $cbb
      )
      = _preferences_general_options(
        $windowr->style_get('content-area-border') );
    $notebook->append_page( $vbox2, Gtk3::Label->new( __('General options') ) );

    $windowr->add_actions(
        'gtk-apply',
        sub {
            $windowr->hide;
            if ( $SETTING{frontend} ne $combob->get_active_index ) {
                $SETTING{frontend} = $combob->get_active_index;
                $windows->hide;
                undef $windows;
            }
            else {
                $SETTING{'visible-scan-options'} = ();
                $SETTING{'scan-reload-triggers'} = ();
                for ( @{ $option_visibility_list->{data} } ) {
                    $SETTING{'visible-scan-options'}{ $_->[0] } = $_->[2];
                    if ( $_->[3] ) {    ## no critic (ProhibitMagicNumbers)
                        push @{ $SETTING{'scan-reload-triggers'} }, $_->[0];
                    }
                }
                if (    $SETTING{frontend} ne 'libsane-perl'
                    and $SETTING{frontend} ne 'libimage-sane-perl' )
                {
                    $SETTING{'scan prefix'}   = $preentry->get_text;
                    $SETTING{'cache options'} = $cbc->get_active;
                    if ( defined $windows ) {
                        $windows->set( 'prefix', $SETTING{'scan prefix'} );
                        $windows->set( 'cache-options',
                            $SETTING{'cache options'} );
                        $windows->set( 'visible-scan-options',
                            $SETTING{'visible-scan-options'} );
                        $windows->set( 'reload-triggers',
                            $SETTING{'scan-reload-triggers'} );
                    }
                    if ( defined $SETTING{cache}
                        and not $SETTING{'cache options'} )
                    {
                        delete $SETTING{cache};
                    }
                }
            }
            $SETTING{'auto-open-scan-dialog'} = $cbo->get_active;

            try {
                my $text = $blacklist->get_text;
                'dummy_device' =~ /$text/xsm;
            }
            catch {
                my $msg =
                  __(
                    "Invalid regex. Try without special characters such as '*'"
                  );
                $logger->warn($msg);
                show_message_dialog(
                    parent           => $windowr,
                    type             => 'error',
                    buttons          => 'close',
                    text             => $msg,
                    'store-response' => TRUE
                );
                $blacklist->set_text( $SETTING{'device blacklist'} );
            };
            $SETTING{'device blacklist'} = $blacklist->get_text;

            $SETTING{'cycle sane handle'}    = $cbcsh->get_active;
            $SETTING{'allow-batch-flatbed'}  = $cb_batch_flatbed->get_active;
            $SETTING{'cancel-between-pages'} = $cb_cancel_btw_pages->get_active;
            $SETTING{'default filename'}     = $fileentry->get_text;
            $SETTING{'restore window'}       = $cbw->get_active;
            $SETTING{use_timezone}           = $cbtz->get_active;
            $SETTING{use_time}               = $cbtm->get_active;
            $SETTING{set_timestamp}          = $cbts->get_active;
            $SETTING{to_png}                 = $cbtp->get_active;
            $SETTING{'convert whitespace to underscores'} = $cbb->get_active;

            if ( defined $windows ) {
                if (   $SETTING{frontend} eq 'libsane-perl'
                    or $SETTING{frontend} eq 'libimage-sane-perl' )
                {
                    $windows->set( 'cycle-sane-handle',
                        $SETTING{'cycle sane handle'} );
                    $windows->set( 'cancel-between-pages',
                        $SETTING{'cancel-between-pages'} );
                }
                $windows->set( 'allow-batch-flatbed',
                    $SETTING{'allow-batch-flatbed'} );
            }

            if ( defined $windowi ) {
                $windowi->set( 'include-time', $SETTING{use_time} );
            }

            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(
                    parent  => $window,
                    type    => 'warning',
                    buttons => 'close',
                    text    => __(
'You will have to restart gscanp2df for changes to the temporary directory to take effect.'
                    )
                );
            }
            $SETTING{'available-tmp-warning'} = $spinbuttonw->get_value;
            $SETTING{'Blank threshold'}       = $spinbuttonb->get_value;
            $SETTING{'Dark threshold'}        = $spinbuttond->get_value;
            $SETTING{'OCR output'}            = $comboo->get_active_index;

            # Store viewer preferences
            $SETTING{'view files toggle'} = $cbv->get_active;
        },
        'gtk-cancel',
        sub {
            $windowr->hide;
        }
    );
    $windowr->show_all;
    return;
}

sub _preferences_scan_options {
    my ($border_width) = @_;
    my $vbox = Gtk3::VBox->new;
    $vbox->set_border_width($border_width);

    my $cbo =
      Gtk3::CheckButton->new_with_label( __('Open scanner at program start') );
    $cbo->set_tooltip_text(
        __(
'Automatically open the scan dialog in the background at program start. '
              . 'This saves time clicking the scan button and waiting for the program to find the list of scanners'
        )
    );

    if ( defined $SETTING{'auto-open-scan-dialog'} ) {
        $cbo->set_active( $SETTING{'auto-open-scan-dialog'} );
    }
    $vbox->pack_start( $cbo, TRUE, TRUE, 0 );

    # Frontends
    my $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
    my $label = Gtk3::Label->new( __('Frontend') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my @frontends = (
        [
            'libimage-sane-perl',
            __('libimage-sane-perl'),
            __('Scan using the Perl bindings for SANE.')
        ],
        [
            'scanimage', __('scanimage'),
            __('Scan using the scanimage frontend.')
        ],
    );
    if ( $dependencies{scanadf} ) {
        push @frontends,
          [ 'scanadf', __('scanadf'), __('Scan using the scanadf frontend.') ];
    }
    my $combob = Gscan2pdf::ComboBoxText->new_from_array(@frontends);
    $hbox->set_tooltip_text( __('Interface used for scanner access') );
    $hbox->pack_end( $combob, TRUE, TRUE, 0 );

    # Device blacklist
    my $hboxb = Gtk3::HBox->new;
    $vbox->pack_start( $hboxb, FALSE, FALSE, 0 );
    $label = Gtk3::Label->new( __('Device blacklist') );
    $hboxb->pack_start( $label, FALSE, FALSE, 0 );
    my $blacklist = Gtk3::Entry->new;
    $hboxb->add($blacklist);
    $hboxb->set_tooltip_text( __('Device blacklist (regular expression)') );

    if ( defined $SETTING{'device blacklist'} ) {
        $blacklist->set_text( $SETTING{'device blacklist'} );
    }

    # Cycle SANE handle after scan
    my $cbcsh =
      Gtk3::CheckButton->new_with_label( __('Cycle SANE handle after scan') );
    $cbcsh->set_tooltip_text(
        __('Some ADFs do not feed out the last page if this is not enabled') );
    if ( defined $SETTING{'cycle sane handle'} ) {
        $cbcsh->set_active( $SETTING{'cycle sane handle'} );
    }
    $vbox->pack_start( $cbcsh, FALSE, FALSE, 0 );

    # Allow batch scanning from flatbed
    my $cb_batch_flatbed =
      Gtk3::CheckButton->new_with_label(
        __('Allow batch scanning from flatbed') );
    $cb_batch_flatbed->set_tooltip_text(
        __(
'If not set, switching to a flatbed scanner will force # pages to 1.'
        )
    );
    $cb_batch_flatbed->set_active( $SETTING{'allow-batch-flatbed'} );
    $vbox->pack_start( $cb_batch_flatbed, FALSE, FALSE, 0 );

    # Force new scan job between pages
    my $cb_cancel_btw_pages =
      Gtk3::CheckButton->new_with_label(
        __('Force new scan job between pages') );
    $cb_cancel_btw_pages->set_tooltip_text(
        __(
'Otherwise, some Brother scanners report out of documents, despite scanning from flatbed.'
        )
    );
    $cb_cancel_btw_pages->set_active( $SETTING{'cancel-between-pages'} );
    $vbox->pack_start( $cb_cancel_btw_pages, FALSE, FALSE, 0 );
    $cb_cancel_btw_pages->set_sensitive( $SETTING{'allow-batch-flatbed'} );
    $cb_batch_flatbed->signal_connect(
        toggled => sub {
            $cb_cancel_btw_pages->set_sensitive(
                $cb_batch_flatbed->get_active );
        }
    );

    # Select num-pages = all on selecting ADF
    my $cb_adf_all_pages =
      Gtk3::CheckButton->new_with_label(
        __('Select # pages = all on selecting ADF') );
    $cb_adf_all_pages->set_tooltip_text(
        __(
'If this option is enabled, when switching to source=ADF, # pages = all is selected'
        )
    );
    $cb_adf_all_pages->set_active( $SETTING{'adf-defaults-scan-all-pages'} );
    $vbox->pack_start( $cb_adf_all_pages, FALSE, FALSE, 0 );

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

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

    # Clear options cache
    my $buttonc =
      Gtk3::Button->new( __('Clear device-dependent options cache') );
    $vbox->pack_start( $buttonc, FALSE, FALSE, 0 );
    $buttonc->signal_connect(
        clicked => sub {
            if ( defined $SETTING{cache} ) { delete $SETTING{cache} }
            if ( defined $windows ) { $windows->set( 'options-cache', undef ) }
        }
    );

    # Option visibility
    my $oframe = Gtk3::Frame->new( __('Option visibility & control') );
    $vbox->pack_start( $oframe, TRUE, TRUE, 0 );
    my $vvbox = Gtk3::VBox->new;
    $vvbox->set_border_width($border_width);
    $oframe->add($vvbox);
    my $scwin = Gtk3::ScrolledWindow->new;
    $vvbox->pack_start( $scwin, TRUE, TRUE, 0 );
    $scwin->set_policy(qw/never automatic/);
    $option_visibility_list = Gtk3::SimpleList->new(
        __('Title')  => 'text',
        __('Type')   => 'text',
        __('Show')   => 'bool',
        __('Reload') => 'bool'
    );
    $option_visibility_list->get_selection->set_mode('multiple');
    $scwin->add($option_visibility_list);
    my $bhbox = Gtk3::HBox->new;
    $vvbox->pack_start( $bhbox, FALSE, FALSE, 0 );
    my $sbutton = Gtk3::Button->new( __('Show') );
    $sbutton->signal_connect(
        clicked => sub {

            for ( $option_visibility_list->get_selected_indices ) {
                $option_visibility_list->{data}[$_][2] = TRUE;
            }
        }
    );
    $bhbox->pack_start( $sbutton, TRUE, TRUE, 0 );
    my $hbutton = Gtk3::Button->new( __('Hide') );
    $hbutton->signal_connect(
        clicked => sub {
            for ( $option_visibility_list->get_selected_indices ) {
                $option_visibility_list->{data}[$_][2] = FALSE;
            }
        }
    );
    $bhbox->pack_start( $hbutton, TRUE, TRUE, 0 );
    my $fbutton = Gtk3::Button->new( __('List current options') );
    $fbutton->signal_connect(
        clicked => sub {
            if ( defined $windows ) {
                @{ $option_visibility_list->{data} } = ();
                my $options = $windows->get('available-scan-options');
                for my $i ( 1 .. $options->num_options - 1 ) {
                    my $opt = $options->by_index($i);
                    push @{ $option_visibility_list->{data} },
                      [ $opt->{title}, $opt->{type}, TRUE, FALSE ];
                }
                push @{ $option_visibility_list->{data} },
                  [ 'Paper size', undef, TRUE, FALSE ];
            }
            else {
                show_message_dialog(
                    parent  => $windowr,
                    type    => 'error',
                    buttons => 'close',
                    text    => __(
                        'No scanner currently open with command line frontend.')
                );
            }
            return;
        }
    );
    $bhbox->pack_start( $fbutton, TRUE, TRUE, 0 );
    my $show_not_listed =
      Gtk3::CheckButton->new_with_label( __('Show options not listed') );
    $vvbox->pack_start( $show_not_listed, FALSE, FALSE, 0 );

    # fill the list
    if ( defined $SETTING{'visible-scan-options'} ) {
        my %reload;
        if ( ref( $SETTING{'scan-reload-triggers'} ) ne 'ARRAY' ) {
            $SETTING{'scan-reload-triggers'} =
              [ $SETTING{'scan-reload-triggers'} ];
        }
        for ( @{ $SETTING{'scan-reload-triggers'} } ) {
            $reload{$_} = 1;
        }
        for ( sort { $a cmp $b } keys %{ $SETTING{'visible-scan-options'} } ) {
            push @{ $option_visibility_list->{data} },
              [
                $_,                                   undef,
                $SETTING{'visible-scan-options'}{$_}, defined $reload{$_}
              ];
        }
    }

    $combob->signal_connect(
        changed => sub {
            my $libsane_active =
              $combob->get_active_index =~ /lib(:?image-)?sane-perl/xsm;
            $cbcsh->set_sensitive($libsane_active);
            $hboxp->set_sensitive( not $libsane_active );
            $cbc->set_sensitive( not $libsane_active );
            $buttonc->set_sensitive( not $libsane_active );
            $oframe->set_sensitive( not $libsane_active );
        }
    );
    $combob->set_active_index( $SETTING{frontend} );
    return $vbox, \@frontends, $combob, $preentry, $cbc, $cbo, $blacklist,
      $cbcsh, $cb_batch_flatbed, $cb_cancel_btw_pages;
}

sub _preferences_general_options {
    my ($border_width) = @_;
    my $vbox = Gtk3::VBox->new;
    $vbox->set_border_width($border_width);

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

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

    # Default filename
    my $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    my $label = Gtk3::Label->new( __('Default PDF & DjVu filename') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my $fileentry = Gtk3::Entry->new;
    $fileentry->set_tooltip_text(
        __(<<'EOS')
strftime codes, e.g.:
%Y	current year

with the following additions:
%Da	author
%De	filename extension
%Dt	title

All document date codes use strftime codes with a leading D, e.g.:
%DY	document year
%Dm	document month
%Dd	document day
EOS
    );
    $hbox->add($fileentry);
    $fileentry->set_text( $SETTING{'default filename'} );

    # Replace whitespace in filenames with underscores
    my $cbb = Gtk3::CheckButton->new_with_label(
        __('Replace whitespace in filenames with underscores') );
    $cbb->set_active( $SETTING{'convert whitespace to underscores'} );
    $vbox->pack_start( $cbb, TRUE, TRUE, 0 );

    # Timezone
    my $cbtz =
      Gtk3::CheckButton->new_with_label( __('Use timezone from locale') );
    $cbtz->set_active( $SETTING{use_timezone} );
    $vbox->pack_start( $cbtz, TRUE, TRUE, 0 );

    # Time
    my $cbtm =
      Gtk3::CheckButton->new_with_label( __('Specify time as well as date') );
    $cbtm->set_active( $SETTING{use_time} );
    $vbox->pack_start( $cbtm, TRUE, TRUE, 0 );

    # Set file timestamp with metadata
    my $cbts = Gtk3::CheckButton->new_with_label(
        __('Set access and modification times to metadata date') );
    $cbts->set_active( $SETTING{set_timestamp} );
    $vbox->pack_start( $cbts, TRUE, TRUE, 0 );

    # Convert scans from PNM to PNG
    my $cbtp = Gtk3::CheckButton->new_with_label(
        __('Convert scanned images to PNG before further processing') );
    $cbtp->set_active( $SETTING{to_png} );
    $vbox->pack_start( $cbtp, TRUE, TRUE, 0 );

    # Temporary directory settings
    $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( __('Temporary directory') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my $tmpentry = Gtk3::Entry->new;
    $hbox->add($tmpentry);
    $tmpentry->set_text( dirname($session) );
    my $button = Gtk3::Button->new( __('Browse') );
    $button->signal_connect(
        clicked => sub {
            my $file_chooser = Gtk3::FileChooserDialog->new(
                __('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 );

    # Available space in temporary directory
    $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( __('Warn if available space less than (Mb)') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my $spinbuttonw = Gtk3::SpinButton->new_with_range( 0, $_100_000MB, 1 );
    $spinbuttonw->set_value( $SETTING{'available-tmp-warning'} );
    $spinbuttonw->set_tooltip_text(
        __(
'Warn if the available space in the temporary directory is less than this value'
        )
    );
    $hbox->add($spinbuttonw);

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

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

    # OCR output
    $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( __('OCR output') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my @ocr_function = (
        [
            'replace',
            __('Replace'),
            __(
'Replace the contents of the text buffer with that from the OCR output.'
            )
        ],
        [
            'prepend', __('Prepend'),
            __('Prepend the OCR output to the text buffer.')
        ],
        [
            'append', __('Append'),
            __('Append the OCR output to the text buffer.')
        ],
    );
    my $comboo = Gscan2pdf::ComboBoxText->new_from_array(@ocr_function);
    $comboo->set_active_index( $SETTING{'OCR output'} );
    $hbox->pack_end( $comboo, TRUE, TRUE, 0 );

    # Manage user-defined tools
    my $frame = Gtk3::Frame->new( __('Manage user-defined tools') );
    $vbox->pack_start( $frame, TRUE, TRUE, 0 );
    my $vboxt = Gtk3::VBox->new;
    $vboxt->set_border_width($border_width);
    $frame->add($vboxt);

    for my $tool ( @{ $SETTING{user_defined_tools} } ) {
        add_user_defined_tool_entry( $vboxt, [], $tool );
    }
    my $abutton = Gtk3::Button->new_from_stock('gtk-add');
    $vboxt->pack_start( $abutton, TRUE, TRUE, 0 );
    $abutton->signal_connect(
        clicked => sub {
            add_user_defined_tool_entry(
                $vboxt,
                [ $comboboxudt, $windows->{comboboxudt} ],
                'my-tool %i %o'
            );
            $vboxt->reorder_child( $abutton, $EMPTY_LIST );
            update_list_user_defined_tools( $vboxt,
                [ $comboboxudt, $windows->{comboboxudt} ] );
        }
    );
    return $vbox, $fileentry, $cbw, $cbtz, $cbtm, $cbts, $cbtp, $tmpentry,
      $spinbuttonw,
      $spinbuttonb, $spinbuttond, \@ocr_function, $comboo, $cbv, $cbb;
}

sub _cb_array_append {
    my ( $combobox_array, $text ) = @_;
    for my $combobox ( @{$combobox_array} ) {
        if ( defined $combobox ) {
            $combobox->append_text($text);
        }
    }
    return;
}

# Update list of user-defined tools
sub update_list_user_defined_tools {
    my ( $vbox, $combobox_array ) = @_;
    my (@list);
    for my $combobox ( @{$combobox_array} ) {
        if ( defined $combobox ) {
            while ( $combobox->get_num_rows > 0 ) {
                $combobox->remove(0);
            }
        }
    }
    for my $hbox ( $vbox->get_children ) {
        if ( $hbox->isa('Gtk3::HBox') ) {
            for my $widget ( $hbox->get_children ) {
                if ( $widget->isa('Gtk3::Entry') ) {
                    my $text = $widget->get_text;
                    push @list, $text;
                    _cb_array_append( $combobox_array, $text );
                }
            }
        }
    }
    $SETTING{user_defined_tools} = \@list;
    update_post_save_hooks();
    for my $combobox ( @{$combobox_array} ) {
        if ( defined $combobox ) {
            $combobox->set_active_by_text( $SETTING{current_udt} );
        }
    }
    return;
}

# Add user-defined tool entry
sub add_user_defined_tool_entry {
    my ( $vbox, $combobox_array, $tool ) = @_;
    _cb_array_append( $combobox_array, $tool );
    my $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    my $entry = Gtk3::Entry->new;
    $entry->set_text($tool);
    $entry->signal_connect(
        changed => sub {
            update_list_user_defined_tools( $vbox, $combobox_array );
            ()    # this callback must return either 2 or 0 items.
        }
    );

    $entry->set_tooltip_text(
        __(
"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 = Gtk3::Button->new;
    $button->set_image( Gtk3::Image->new_from_stock( 'gtk-delete', 'button' ) );
    $button->signal_connect(
        clicked => sub {
            $hbox->destroy;
            update_list_user_defined_tools( $vbox, $combobox_array );
        }
    );
    $hbox->pack_end( $button, FALSE, FALSE, 0 );
    $hbox->show_all;
    return;
}

sub update_post_save_hooks {
    if ( defined $windowi ) {

        if ( defined $windowi->{comboboxpsh} ) {

            # empty combobox
            for ( 1 .. $windowi->{comboboxpsh}->get_num_rows ) {
                $windowi->{comboboxpsh}->remove(0);
            }
        }
        else {
            # create it
            $windowi->{comboboxpsh} = Gscan2pdf::ComboBoxText->new;
        }

        # fill it again
        for my $tool ( @{ $SETTING{user_defined_tools} } ) {
            if ( $tool !~ /%o/xsm ) {
                $windowi->{comboboxpsh}->append_text($tool);
            }
        }
        $windowi->{comboboxpsh}->set_active_by_text( $SETTING{current_psh} );
    }
    return;
}

sub properties {

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

    $windowp = Gscan2pdf::Dialog->new(
        'transient-for'  => $window,
        title            => __('Properties'),
        'hide-on-delete' => TRUE,
    );
    my $vbox = $windowp->get_content_area;

    my $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    my $label = Gtk3::Label->new( $d_sane->get('X Resolution') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my $xspinbutton = Gtk3::SpinButton->new_with_range( 0, $MAX_DPI, 1 );
    $xspinbutton->set_digits(1);
    $hbox->pack_start( $xspinbutton, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( __('dpi') );
    $hbox->pack_end( $label, FALSE, FALSE, 0 );

    $hbox = Gtk3::HBox->new;
    $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( $d_sane->get('Y Resolution') );
    $hbox->pack_start( $label, FALSE, FALSE, 0 );
    my $yspinbutton = Gtk3::SpinButton->new_with_range( 0, $MAX_DPI, 1 );
    $yspinbutton->set_digits(1);
    $hbox->pack_start( $yspinbutton, TRUE, TRUE, 0 );
    $label = Gtk3::Label->new( __('dpi') );
    $hbox->pack_end( $label, FALSE, FALSE, 0 );

    my ( $xresolution, $yresolution ) = get_selected_properties();
    $logger->debug(
        "get_selected_properties returned $xresolution,$yresolution");
    $xspinbutton->set_value($xresolution);
    $yspinbutton->set_value($yresolution);
    $slist->get_selection->signal_connect(
        changed => sub {
            ( $xresolution, $yresolution ) = get_selected_properties();
            $logger->debug(
                "get_selected_properties returned $xresolution,$yresolution");
            $xspinbutton->set_value($xresolution);
            $yspinbutton->set_value($yresolution);
        }
    );

    $windowp->add_actions(
        'gtk-apply',
        sub {
            $windowp->hide;
            $xresolution = $xspinbutton->get_value;
            $yresolution = $yspinbutton->get_value;
            $slist->get_model->signal_handler_block(
                $slist->{row_changed_signal} );
            for ( $slist->get_selected_indices ) {
                $logger->debug(
"setting resolution $xresolution,$yresolution for page $slist->{data}[$_][0]"
                );
                $slist->{data}[$_][2]{xresolution} = $xresolution;
                $slist->{data}[$_][2]{yresolution} = $yresolution;
            }
            $slist->get_model->signal_handler_unblock(
                $slist->{row_changed_signal} );
        },
        'gtk-cancel',
        sub {
            $windowp->hide;
        }
    );
    $windowp->show_all;
    return;
}

# Helper function for properties()
sub get_selected_properties {
    my @page        = $slist->get_selected_indices;
    my $xresolution = $EMPTY;
    my $yresolution = $EMPTY;
    if ( @page > 0 ) {
        my $page = shift @page;
        $xresolution = $slist->{data}[$page][2]{xresolution};
        $yresolution = $slist->{data}[$page][2]{yresolution};
        $logger->debug(
"Page $slist->{data}[$page][0] has resolutions $xresolution,$yresolution"
        );
    }
    for (@page) {
        if ( $slist->{data}[$_][2]{xresolution} != $xresolution ) {
            $xresolution = $EMPTY;
            last;
        }
    }
    for (@page) {
        if ( $slist->{data}[$_][2]{yresolution} != $yresolution ) {
            $yresolution = $EMPTY;
            last;
        }
    }

    # round the value to a sensible number of significant figures
    return $xresolution eq $EMPTY ? 0 : sprintf( '%.1g', $xresolution ),
      $yresolution eq $EMPTY ? 0 : sprintf '%.1g', $yresolution;
}

# Helper function to display a message dialog, wait for a response, and return it

sub ask_question {
    my %options = @_;

    # replace any numbers with metacharacters to compare to filter
    my $text =
      Gscan2pdf::Dialog::MultipleMessage::filter_message( $options{text} );
    if (
        Gscan2pdf::Dialog::MultipleMessage::response_stored(
            $text, $SETTING{message}
        )
      )
    {
        return $SETTING{message}{$text}{response};
    }

    my $cb;
    my $dialog =
      Gtk3::MessageDialog->new( $options{parent},
        [ 'destroy-with-parent', 'modal' ],
        $options{type}, $options{buttons}, $options{text} );
    if ( $options{'store-response'} ) {
        $cb = Gtk3::CheckButton->new_with_label(
            __("Don't show this message again") );
        $dialog->get_message_area->add($cb);
    }
    if ( defined $options{'default-response'} ) {
        $dialog->set_default_response( $options{'default-response'} );
    }
    $dialog->show_all;
    my $response = $dialog->run;
    $dialog->destroy;
    if ( $options{'store-response'} and $cb->get_active ) {
        my $filter = TRUE;
        if ( $options{'stored-responses'} ) {
            $filter = FALSE;
            for ( @{ $options{'stored-responses'} } ) {
                if ( $_ eq $response ) {
                    $filter = TRUE;
                    last;
                }
            }
        }
        if ($filter) {
            $SETTING{message}{$text}{response} = $response;
        }
    }
    return $response;
}

sub show_message_dialog {
    my %options = @_;
    if ( not defined $message_dialog ) {
        $message_dialog = Gscan2pdf::Dialog::MultipleMessage->new(
            title           => __('Messages'),
            'transient-for' => $options{parent}
        );
        $message_dialog->set_default_size( $SETTING{message_window_width},
            $SETTING{message_window_height} );
    }

    $options{responses} = $SETTING{message};
    $message_dialog->add_message(%options);

    my $response;
    if ( $message_dialog->{grid_rows} > 1 ) {
        $message_dialog->show_all;
        $response = $message_dialog->run;
    }

    if ( defined $message_dialog ) {    # could be undefined for multiple calls
        $message_dialog->store_responses( $response, $SETTING{message} );
        ( $SETTING{message_window_width}, $SETTING{message_window_height} ) =
          $message_dialog->get_size;
        $message_dialog->destroy;
        undef $message_dialog;
    }
    return;
}

__END__

=encoding utf8

=head1 NAME

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

=for html <p align="center">
 <img src="https://a.fsdn.com/con/app/proj/gscan2pdf/screenshots/Screenshot.png/max/max/1" border="1" width="632"
 height="480" alt="Screenshot" /><br/>Screenshot: Main page v2.4.0</p>

=head1 USAGE

=over

=item 1. Scan one or several pages in with File/Scan

=item 2. Create PDF of selected pages with File/Save

=back

=head1 REQUIRED ARGUMENTS

None

=head1 OPTIONS

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 --import=<PDF|DjVu|image>
Imports the specified file

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

To diagnose a possible error, start gscan2pdf from the command line with logging enabled:

C<gscan2pdf --log=file.log>

and check file.log.

=head1 EXIT STATUS

None

=head1 CONFIGURATION

gscan2pdf creates a text resource file in ~/.config/gscan2pdfrc. The directory
can be changed by setting the $XDG_CONFIG_HOME variable. Generally, however,
preferences should be changed via the Edit/Preferences menu, or are captured
automatically during normal usage of the program.

=head1 INCOMPATIBILITIES

None known.

=head1 BUGS AND LIMITATIONS

Whilst it is possible to import PDFs, this is intended to be able to round-trip
files created by gscan2pdf.

=head1 Download

gscan2pdf is available on Sourceforge
(L<https://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, you can automatically keep up to date
with the latest version via the ppa:

C<sudo apt-add-repository ppa:jeffreyratcliffe/ppa>

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.

From the command line:

C<sudo apt-get update>

C<sudo apt-get install gscan2pdf>

=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<https://sourceforge.net/projects/gscan2pdf/files/>).

=head2 From the repository

gscan2pdf uses Git for its Revision Control System. You can browse the
tree at L<https://sourceforge.net/p/gscan2pdf/code/>.

Git users can clone the complete tree with
C<git clone git://git.code.sf.net/p/gscan2pdf/code>

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

C<make test> should run several hundred tests to confirm that things will work
properly on your system.

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 libgtk3-perl >= 0.028

There is a bug in version of libgtk3-perl before 0.028 that causes gscan2pdf to
crash when saving. Whilst I could prevent gscan2pdf from crashing, it would
still be impossible to save anything, rendering gscan2pdf rather useless.

=item libgtk3-simplelist-perl

A simple interface to Gtk3's complex MVC list widget

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

Using libc functions for internationalisation in Perl

=item libpdf-api2-perl

provides the functions for creating PDF documents in Perl

=item libsane

API library for scanners

=item libimage-sane-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 unpaper

post-processing tool for scanned pages. See L<https://www.flameeyes.eu/projects/unpaper>.

=item xdg-utils

Desktop integration utilities from freedesktop.org. Required for Email as PDF.
See L<https://www.freedesktop.org/wiki/Software/xdg-utils/>

=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<https://github.com/tesseract-ocr/tesseract>

=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<https://lists.sourceforge.net/lists/listinfo/gscan2pdf-announce>

=item gscan2pdf-help

General support, questions, etc.. You can subscribe at
L<https://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.
The Debian tool "reportbug" provides a convenient GUI for doing so.

=over

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

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

=back

Alternatively, there is a bug tracker for the gscan2pdf project on
Sourceforge (L<https://sourceforge.net/p/gscan2pdf/_list/tickets?source=navbar>).

Please include the log file created by C<gscan2pdf --log=log> with any new bug report.

=head1 Translations

gscan2pdf has already been partly translated into 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 DESCRIPTION

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

Note that files can also be imported by dragging them into the thumbnail list
from a program like nautilus or konqueror.

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

When an image is scanned, gscan2pdf attempts to extract the resolution from the
scan options. This nearly always works without problem.

Importing an image can be trickier, however. Some image formats such as PNM do
not encode metadata for resolution. In other cases, the data is incorrect.
Edit/Properties allows the user to manually correct the metadata for a
particular page, thus correcting the size of final PDF or DjVu. The image
itself is otherwise not changed - it is not down- or upscaled.

=head3 Preferences

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

=head4 Frontends

gscan2pdf initially supported 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. These are called
libsane-perl.

Before 1.2.0, options available through CLI frontends like scanimage were made
visible as users asked for them. In 1.2.0, all options can be shown or hidden
via Edit/Preferences, along with the ability to specify which options trigger a
reload.

In 1.8.3, New Perl bindings for SANE were introduced. These are called
libimage-sane-perl and are the preferred frontend.

In 1.8.5, support for libsane-perl was removed.

=head4 Device blacklist

Ignore listed devices.

Note that this is a device name regular expression, e.g. /dev/video, and not the
name as listed in the scan window, e.g. Noname Integrated_Webcam_HD.

=head4 Default filename for PDF or DjVu files

All strftime codes (e.g. %Y for the current year) are available as variables,
with the following additions:

 %Da	author
 %De	filename extension
 %Dt	title

All document date codes use strftime codes with a leading D, e.g.:

 %DY	document year
 %Dm	document month
 %Dd	document 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<https://www.flameeyes.eu/projects/unpaper>) 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<https://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?

In Edit/Preferences, tick the box "Allow batch scanning from flatbed".

Some Brother scanners report "out of documents", despite scanning from flatbed.
This can be worked around by ticking the box
"Force new scan job between pages".

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.

If you are quick, you might be able to change the document on the flatbed whilst
the scan head is returning.

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

=head2 How can I search for text in the OCR layer of the finished PDF or DJVU file?

pdftotext or djvutxt can extract the text layer from PDF or DJVU files. See the
respective man pages for details.

Having opened a PDF or DJVU file in evince or Acrobat Reader, the search
function will typically find the page with the requested text and highlight it.

There are various tools for searching or indexing files, including PDF and DJVU:

=over

=item *
(meta) Tracker (L<https://projects.gnome.org/tracker/>)

=item *
plone (L<http://plone.org/>)

=item *
pdfgrep (L<http://pdfgrep.sourceforge.net/>

=item *
swish-e (L<http://www.swish-e.org/>)

=item *
recoll (L<http://www.lesbonscomptes.com/recoll/>)

=item *
terrier (L<http://www.lesbonscomptes.com/recoll/>)

=back

=head2 How can I change the colour of the selection box in the image viewer?

Create a file called C<~/.config/gtk-3.0/gtk.css> with the following content:

 .rubberband,
 rubberband,
 flowbox rubberband,
 treeview.view rubberband,
 .content-view rubberband,
 .content-view .rubberband {
   border: 1px solid #2a76c6;
   background-color: rgba(42, 118, 198, 0.2); }

=head2 How can I change the colour of the OCR output

Create a file called C<~/.config/gtk-3.0/gtk.css> with the following content:

#gscan2pdf-ocr-output {
  color: black;
}

=head1 See Also

XSane (L<http://xsane.org/>)

Scan Tailor (L<http://scantailor.org/>)

=head1 Author

Jeffrey Ratcliffe (jffry at posteo dot net)

=head1 Thanks to

=over

=item *
all the people who have sent patches, translations, bugs and feedback.

=item *
the gtk+ project for a most excellent graphics toolkit.

=item *
the Gtk3-Perl project for their superb Perl bindings for GTK3.

=item *
The SANE project for scanner access

=item *
BjE<ouml>rn Lindqvist for the gtkimageview widget

=item *
Sourceforge for hosting the project.

=back

=for html <hr />
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="lc" value="US">
<input type="hidden" name="cmd" value="_s-xclick">
<input type="hidden" name="hosted_button_id" value="GYQGXYD5UZS6S">
<input type="image" src="https://www.paypalobjects.com/en_US/DE/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
</form>

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2006--2019 Jeffrey Ratcliffe <jffry@posteo.net>

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 <https://www.gnu.org/licenses/>.

=cut
