lumberjack55

Untitled

Mar 31st, 2013
235
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 6 253.94 KB | None | 0 0
  1. #!/usr/bin/perl
  2.  
  3. eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
  4.     if 0; # not running under some shell
  5.  
  6. # gscan2pdf --- to aid the scan to PDF or DjVu process
  7. # Copyright (C) 2006--2013 Jeffrey Ratcliffe <[email protected]>
  8.  
  9. # This program is free software: you can redistribute it and/or modify
  10. # it under the terms of the version 3 GNU General Public License as
  11. # published by the Free Software Foundation.
  12.  
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16. # GNU General Public License for more details.
  17.  
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  20.  
  21. # Release procedure:
  22. #    Use
  23. #      make tidy
  24. #      TEST_AUTHOR=1 make test
  25. #    immediately before release so as not to affect any patches
  26. #    in between, and then consistently before each commit afterwards.
  27. # 0. Test scan in lineart, greyscale and colour.
  28. # 1. New screendump required? Print screen creates screenshot.png in Desktop.
  29. #    Download new translations (https://translations.launchpad.net/gscan2pdf)
  30. #    Upload .pot
  31. #    Update translators in credits (https://launchpad.net/gscan2pdf/+topcontributors)
  32. #    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>
  33. #    Check $version
  34. #    Make appropriate updates to debian/changelog
  35. # 2. perl Makefile.PL
  36. #    make rpmdist debdist
  37. #    test dist sudo dpkg -i gscan2pdf_x.x.x_all.deb
  38. # 3. git status
  39. #    git tag vx.x.x
  40. #    git push --tags ssh://[email protected]/p/gscan2pdf/code master
  41. # 4. make remote-html
  42. # 5. create version directory in https://sourceforge.net/projects/gscan2pdf/files/gscan2pdf
  43. #    make file_releases
  44. # 6. Freshmeat freshmeat.net/projects/gscan2pdf/releases/new (requires summary of changes)
  45. # 7. Launchpad (https://launchpad.net/gscan2pdf/trunk/+addrelease)
  46. # 8. make ppa
  47. #    name the release -0~ppa1<release>, where release is precise, hardy, etc
  48. #    debuild -S -sa, dput gscan2pdf-ppa .changes
  49. #    https://launchpad.net/~jeffreyratcliffe/+archive
  50. #    ftp-master (check contents with dpkg-deb --contents)
  51.  
  52. use warnings;
  53. use strict;
  54. use feature "switch";
  55.  
  56. use Gscan2pdf::Document;
  57. use Gscan2pdf::Frontend::Sane;
  58. use Gscan2pdf::Frontend::CLI;
  59. use Gscan2pdf::NetPBM;
  60. use Gscan2pdf::Scanner::Options;
  61. use Gscan2pdf::Dialog::Scan::Sane;
  62. use Gscan2pdf::Tesseract;
  63. use Gscan2pdf::Ocropus;
  64. use Gscan2pdf::Cuneiform;
  65. use Gscan2pdf::Unpaper;
  66. use Gscan2pdf::Dialog::Renumber;
  67. use Sane 0.05;    # To get SANE_* enums
  68. use Gtk2::ImageView;
  69. use Goo::Canvas;
  70. use Gtk2::Ex::Simple::List;
  71. use Gtk2 -init;    # Could just call init separately
  72. use Gtk2::Gdk::Keysyms;
  73. use Cwd;                    # To obtain current working directory
  74. use File::Basename;         # Split filename into dir, file, ext
  75. use File::Copy;
  76. use File::Temp;             # To create temporary files
  77. use Glib qw(TRUE FALSE);    # To get TRUE and FALSE
  78. use Config::General 2.40;
  79. use PDF::API2;
  80. use Getopt::Long;
  81. use Set::IntSpan 1.10;      # For size method for page numbering issues
  82. use Proc::Killfam;
  83. use Fcntl qw(:flock)
  84.   ;    # import LOCK_* constants to prevent us clobbering running instances
  85. use Log::Log4perl;
  86. use Try::Tiny;
  87. use Data::Dumper;
  88.  
  89. # To sort out LC_NUMERIC and $SIG{CHLD}
  90. use POSIX qw(locale_h :signal_h :errno_h :sys_wait_h);
  91. use Locale::gettext 1.05;    # For translations
  92.  
  93. Glib::set_application_name('gscan2pdf');
  94. my $prog_name = Glib::get_application_name;
  95. my $version   = '1.1.3';
  96.  
  97. # Window parameters
  98. my $border_width = 6;
  99.  
  100. # Image border to ensure that a scaled to fit image gets no scrollbars
  101. my $border = 1;
  102.  
  103. # Set up domain for gettext (internationalisation)
  104. # Expects /usr/share/locale/LANGUAGE/LC_MESSAGES/$prog_name.mo
  105. # or whatever is set by $d->dir([NEWDIR]);
  106. my $d      = Locale::gettext->domain($prog_name);
  107. my $d_sane = Locale::gettext->domain('sane-backends');
  108.  
  109. my $debug = FALSE;
  110. my ( $test, $help, $log, $log_level, @device,
  111.  $locale, @device_list, $test_image );
  112. my @args = (
  113.  'device=s'     => \@device,
  114.  'test=s%'      => \$test,
  115.  'test-image=s' => \$test_image,
  116.  'locale=s'     => \$locale,
  117.  'help'         => \$help,
  118.  'log=s'        => \$log,
  119.  'debug'        => sub { $log_level = 'DEBUG'; $debug = TRUE },
  120.  'info'  => sub { $log_level = 'INFO' },
  121.  'warn'  => sub { $log_level = 'WARN' },
  122.  'error' => sub { $log_level = 'ERROR' },
  123.  'fatal' => sub { $log_level = 'FATAL' },
  124.  'version' => sub { warn "$prog_name $version\n"; exit 0 },
  125. );
  126. exit 1 unless ( GetOptions(@args) );
  127. $Sane::DEBUG = $debug;
  128.  
  129. if ( not defined($log_level) ) {
  130.  if ( defined $log ) {
  131.   $log_level = 'DEBUG';
  132.   $debug     = TRUE;
  133.  }
  134.  else {
  135.   $log_level = 'ERROR';
  136.  }
  137. }
  138. my $log_conf = <<'EOS';
  139.  log4perl.appender.Screen        = Log::Log4perl::Appender::Screen
  140.  log4perl.appender.Screen.layout = Log::Log4perl::Layout::SimpleLayout
  141. EOS
  142.  
  143. if ( defined $log ) {
  144.  $log_conf .= <<"EOS";
  145.  log4perl.appender.Logfile          = Log::Log4perl::Appender::File
  146.  log4perl.appender.Logfile.filename = $log
  147.  log4perl.appender.Logfile.mode     = write
  148.  log4perl.appender.Logfile.layout   = Log::Log4perl::Layout::SimpleLayout
  149.  log4perl.category                  = $log_level, Logfile, Screen
  150. EOS
  151. }
  152. else {
  153.  $log_conf .= <<"EOS";
  154.  log4perl.category                  = $log_level, Screen
  155. EOS
  156. }
  157.  
  158. if ( defined $help ) {
  159.  system("perldoc $0") == 0 or die $d->get('Error displaying help'), "\n";
  160. }
  161.  
  162. Log::Log4perl::init( \$log_conf );
  163. my $logger = Log::Log4perl::get_logger();
  164.  
  165. $logger->info("Starting $prog_name $version");
  166. $logger->info("Log level $log_level");
  167.  
  168. if ( defined $locale ) {
  169.  if ( $locale =~ /^\//x ) {
  170.   $d->dir($locale);
  171.  }
  172.  else {
  173.   $d->dir( getcwd . "/$locale" );
  174.  }
  175. }
  176.  
  177. for (@device) {
  178.  push @device_list, { name => $_, label => $_ };
  179. }
  180.  
  181. # Set LC_NUMERIC to C to prevent decimal commas (or anything else) confusing
  182. # scanimage
  183. setlocale( LC_NUMERIC, "C" );
  184. $logger->info( "Using ", setlocale(LC_CTYPE), " locale" );
  185. $logger->info( "Startup LC_NUMERIC ", setlocale(LC_NUMERIC) );
  186.  
  187. # Read config file
  188. my $rc = "$ENV{'HOME'}/.$prog_name";
  189. $logger->info("Reading config from $ENV{'HOME'}/.$prog_name");
  190. system("touch $rc") unless ( -r $rc );
  191. my $conf;
  192. our %SETTING;
  193. try {
  194.  $conf = Config::General->new(
  195.   -ConfigFile  => $rc,
  196.   -SplitPolicy => 'equalsign',
  197.   -UTF8        => 1,
  198.  );
  199. }
  200. catch {
  201.  $logger->error(
  202.   $d->get(
  203.    "Error: unable to load settings.\nBacking up settings\nReverting to defaults"
  204.   )
  205.  );
  206.  move( $rc, "$rc.old" );
  207. }
  208. finally {
  209.  %SETTING = $conf->getall unless (@_);
  210. };
  211.  
  212. my %default_settings = (
  213.  'window_width'      => 800,
  214.  'window_height'     => 600,
  215.  'window_maximize'   => TRUE,
  216.  'thumb panel'       => 100,
  217.  'Page range'        => 'all',
  218.  'layout'            => 'single',
  219.  'downsample'        => FALSE,
  220.  'downsample dpi'    => 150,
  221.  'threshold tool'    => 80,
  222.  'unsharp radius'    => 0,
  223.  'unsharp sigma'     => 1,
  224.  'unsharp amount'    => 1,
  225.  'unsharp threshold' => 0.05,
  226.  'cache options'     => TRUE,
  227.  'restore window'    => TRUE,
  228.  'startup warning'   => FALSE,
  229.  'date offset'       => 0,
  230.  'pdf compression'   => 'auto',
  231.  'quality'           => 75,
  232.  'pages to scan'     => 1,
  233.  'unpaper on scan'   => TRUE,
  234.  'OCR on scan'       => TRUE,
  235.  'frontend'          => 'libsane-perl',
  236.  'rotate facing'     => 0,
  237.  'rotate reverse'    => 0,
  238.  'default filename'  => '%a %y-%m-%d',
  239.  'scan prefix'       => '',
  240.  'Blank threshold'   => 0.005,         # Blank page standard deviation threshold
  241.  'Dark threshold'    => 0.12,          # Dark page mean threshold
  242.  'ocr engine'        => 'ocropus',
  243.  'OCR output' =>
  244.    'replace',    # When a page is re-OCRed, replace old text with new text
  245.  'Paper' => {
  246.   $d->get('A4') => {
  247.    x => 210,
  248.    y => 297,
  249.    l => 0,
  250.    t => 0,
  251.   },
  252.   $d->get('US Letter') => {
  253.    x => 216,
  254.    y => 279,
  255.    l => 0,
  256.    t => 0,
  257.   },
  258.   $d->get('US Legal') => {
  259.    x => 216,
  260.    y => 356,
  261.    l => 0,
  262.    t => 0,
  263.   },
  264.  },
  265.  user_defined_tools => ['gimp %i']
  266. );
  267. delete $SETTING{frontend}
  268.   if (
  269.  defined $SETTING{frontend}
  270.  and ( $SETTING{frontend} eq 'scanimage-perl'
  271.   or $SETTING{frontend} eq 'scanadf-perl'
  272.   or $SETTING{frontend} eq 'scanimage.pl'
  273.   or $SETTING{frontend} eq
  274.   'scanadf.pl' )    # these frontends are just for testing purposes
  275.   );
  276. for ( keys(%default_settings) ) {
  277.  $SETTING{$_} = $default_settings{$_} unless ( defined $SETTING{$_} );
  278. }
  279.  
  280. # Remove invalid paper sizes
  281. for my $paper ( keys %{ $SETTING{Paper} } ) {
  282.  if ( $paper eq '<>' or $paper eq '</>' ) {
  283.   delete $SETTING{Paper}{$paper};
  284.  }
  285.  else {
  286.   for (qw(x y t l)) {
  287.    unless ( defined $SETTING{Paper}{$paper}{$_} ) {
  288.     delete $SETTING{Paper}{$paper};
  289.     last;
  290.    }
  291.   }
  292.  }
  293. }
  294.  
  295. # Delete the options cache if there is a new version of SANE
  296. if (
  297.  (
  298.   defined( $SETTING{'SANE version'} )
  299.   and $SETTING{'SANE version'} ne join( '.', Sane->get_version )
  300.  )
  301.  or ( defined( $SETTING{'libsane-perl version'} )
  302.   and $SETTING{'libsane-perl version'} ne $Sane::VERSION )
  303.   )
  304. {
  305.  delete $SETTING{cache} if ( defined $SETTING{cache} );
  306. }
  307. $SETTING{'SANE version'} = join( '.', Sane->get_version );
  308. $SETTING{'libsane-perl version'} = $Sane::VERSION;
  309.  
  310. # Set up test mode and make sure file has absolute path and is readable
  311. if ( keys %{$test} ) {
  312.  $SETTING{frontend} = 'scanimage-perl';
  313.  for my $file ( keys %{$test} ) {
  314.   my $device = $test->{$file};
  315.   delete $test->{$file};
  316.  
  317.   # Find a way of emulating the nonsense \n that some people seem to get
  318.   $device =~ s/\\n/\n/g;
  319.   $logger->debug("'$file','$device'");
  320.   $file = getcwd . "/$file" if ( $file !~ /^\//x );
  321.   if ( !-r $file ) {
  322.    $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $file ) );
  323.    exit 1;
  324.   }
  325.   push @{ $test->{file} }, $file;
  326.   $test->{output} = '' unless ( defined( $test->{output} ) );
  327.   $test->{output} .=
  328.     "'$#{$test->{file}}','$device','" . basename($file) . "'\n";
  329.  }
  330. }
  331.  
  332. # GetOptions leaves $test as a reference to an empty hash.
  333. else {
  334.  undef $test;
  335. }
  336.  
  337. if ( defined $test_image ) {
  338.  $test_image = expand_tildes($test_image);
  339.  if ( -r $test_image ) {
  340.   $logger->info("Using test image $test_image");
  341.  }
  342.  else {
  343.   $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $test_image ) );
  344.   exit 1;
  345.  }
  346. }
  347.  
  348. $logger->info("Perl version $^V");
  349. $logger->info("Glib-Perl version $Glib::VERSION");
  350. $logger->info( 'Built for Glib ' . join( '.', Glib->GET_VERSION_INFO ) );
  351. $logger->info( 'Running with Glib '
  352.    . join( '.', Glib::major_version, Glib::minor_version, Glib::micro_version )
  353. );
  354. $logger->info("Gtk2-Perl version $Gtk2::VERSION");
  355. $logger->info( "Built for GTK " . join( '.', Gtk2->GET_VERSION_INFO ) );
  356. $logger->info( "Running with GTK " . join( '.', Gtk2->get_version_info ) );
  357. $logger->info( "Using GtkImageView version ",
  358.  Gtk2::ImageView->library_version );
  359. $logger->info("Using Gtk2::ImageView version $Gtk2::ImageView::VERSION");
  360. $logger->info("Using PDF::API2 version $PDF::API2::VERSION");
  361. $logger->info( 'Using Sane version ' . join( '.', Sane->get_version ) );
  362. $logger->info("Using libsane-perl version $Sane::VERSION");
  363.  
  364. $logger->debug( Dumper( \%SETTING ) ) if ($debug);
  365.  
  366. # Just in case dependencies have changed, put put startup warning again
  367. if ( not defined( $SETTING{version} ) or $SETTING{version} ne $version ) {
  368.  $SETTING{'startup warning'} = TRUE;
  369.  delete $SETTING{cache} if ( defined $SETTING{cache} );
  370. }
  371. $SETTING{version} = $version;
  372.  
  373. # Initialise thread handler
  374. Gscan2pdf::Document->setup($logger);
  375.  
  376. # Initialise SANE frontends
  377. Gscan2pdf::Frontend::Sane->setup($logger);
  378. Gscan2pdf::Frontend::CLI->setup($logger);
  379.  
  380. # Update list in Gscan2pdf::Document so that it can be used by get_resolution
  381. Gscan2pdf::Document->set_paper_sizes( $SETTING{Paper} );
  382.  
  383. # Create icons for rotate buttons
  384. my $IconFactory = undef;
  385. my $iconpath;
  386. if ( -d '/usr/share/gscan2pdf' ) {
  387.  $iconpath = '/usr/share/gscan2pdf';
  388. }
  389. else {
  390.  $iconpath =
  391.    '.';    # FIXME: make this a big cleverer, going one dir down from bin.
  392. }
  393. init_icons(
  394.  [ 'rotate90',  "$iconpath/stock-rotate-90.svg" ],
  395.  [ 'rotate180', "$iconpath/stock-rotate-180.svg" ],
  396.  [ 'rotate270', "$iconpath/stock-rotate-270.svg" ],
  397.  [ 'scanner',   "$iconpath/scanner.svg" ],
  398.  [ 'pdf',       "$iconpath/pdf.svg" ],
  399.  [ 'selection', "$iconpath/stock-selection-all-16.png" ],
  400. );
  401.  
  402. # Define application-wide variables here so that they can be referenced
  403. # in the menu callbacks
  404. my (
  405.  $slist, $windowi, $windowe, $windows, $windows2, $windowh, $windowo, $windowrn,
  406.  $windowu, $vboxd, $labeld, $combobd, $combobp, $vbox_manual_paper,
  407.  @undo_buffer,
  408.  @redo_buffer, @undo_selection, @redo_selection, %dependencies,
  409.  %helperTag,   $menubar,        $toolbar,
  410.  @ocr_engine, $bscanall,       $bscannum, @clipboard, $windowd,
  411.  $windowr,    $view,           $frames,   $duplex,    $windowp,
  412.  $batch_scan, $print_settings, $windowc,  $current_page,
  413.  
  414.  # Spin buttons on scan dialog
  415.  $spin_buttons, $start, $spin_buttoni, $spin_buttonn,
  416.  
  417.  # Goo::Canvas for OCR output
  418.  $canvas,
  419.  
  420.  # Scan button on scan dialog
  421.  $scan_button,
  422.  
  423.  # List of PageRange widgets
  424.  @prlist,
  425.  
  426.  # List of user-defined tools
  427.  @user_defined_tools,
  428.  
  429.  # Spinbuttons for selector on crop dialog
  430.  $sb_selector_x, $sb_selector_y, $sb_selector_w, $sb_selector_h,
  431.  
  432.  # dir below session dir
  433.  $tmpdir,
  434.  
  435.  # session dir
  436.  $session,
  437.  
  438.  # filehandle for session lockfile
  439.  $lockfh,
  440.  
  441.  # Temp::File object for PDF to be emailed
  442.  # Define here to make sure that it doesn't get deleted until the next email
  443.  # is created or we quit
  444.  $pdf,
  445. );
  446.  
  447. my @action_items = (
  448.  
  449.  # Fields for each action item:
  450.  # [name, stock_id, value, label, accelerator, tooltip, callback]
  451.  
  452.  # File menu
  453.  [ 'File', undef, $d->get('_File') ],
  454.  [
  455.   'New',                       'gtk-new',
  456.   $d->get('_New'),             '<control>n',
  457.   $d->get('Clears all pages'), \&new
  458.  ],
  459.  [
  460.   'Open',                        'gtk-open',
  461.   $d->get('_Open'),              '<control>o',
  462.   $d->get('Open image file(s)'), \&open_dialog
  463.  ],
  464.  [
  465.   'Scan',
  466.   'scanner',
  467.   $d->get('S_can'),
  468.   '<control>g',
  469.   $d->get('Scan document'),
  470.   sub { $SETTING{frontend} eq 'libsane-perl' ? scan_dialog2() : scan_dialog() }
  471.  ],
  472.  [
  473.   'Save',          'gtk-save', $d->get('Save'), '<control>s',
  474.   $d->get('Save'), \&save_dialog
  475.  ],
  476.  [
  477.   'Email as PDF',                          'gtk-edit',
  478.   $d->get('_Email as PDF'),                '<control>e',
  479.   $d->get('Attach as PDF to a new email'), \&email
  480.  ],
  481.  [
  482.   'Print',          'gtk-print', $d->get('_Print'), '<control>p',
  483.   $d->get('Print'), \&print_dialog
  484.  ],
  485.  [
  486.   'Compress',                           undef,
  487.   $d->get('_Compress temporary files'), undef,
  488.   $d->get('Compress temporary files'),  \&compress_temp
  489.  ],
  490.  [
  491.   'Quit',           'gtk-quit',
  492.   $d->get('_Quit'), '<control>q',
  493.   $d->get('Quit'), sub { Gtk2->main_quit if quit(); }
  494.  ],
  495.  
  496.  # Edit menu
  497.  [ 'Edit', undef, $d->get('_Edit') ],
  498.  [
  499.   'Undo', 'gtk-undo', $d->get('_Undo'), '<control>z', $d->get('Undo'), \&undo
  500.  ],
  501.  [
  502.   'Redo',           'gtk-redo',
  503.   $d->get('_Redo'), '<shift><control>z',
  504.   $d->get('Redo'),  \&unundo
  505.  ],
  506.  [
  507.   'Cut',                    'gtk-cut',
  508.   $d->get('Cu_t'),          '<control>x',
  509.   $d->get('Cut selection'), \&cut_selection
  510.  ],
  511.  [
  512.   'Copy',                    'gtk-copy',
  513.   $d->get('_Copy'),          '<control>c',
  514.   $d->get('Copy selection'), \&copy_selection
  515.  ],
  516.  [
  517.   'Paste',                    'gtk-paste',
  518.   $d->get('_Paste'),          '<control>v',
  519.   $d->get('Paste selection'), \&paste_selection
  520.  ],
  521.  [
  522.   'Delete',                         'gtk-delete',
  523.   $d->get('_Delete'),               undef,
  524.   $d->get('Delete selected pages'), \&delete_pages
  525.  ],
  526.  [
  527.   'Renumber',                'gtk-sort-ascending',
  528.   $d->get('_Renumber'),      '<control>r',
  529.   $d->get('Renumber pages'), \&renumber_dialog
  530.  ],
  531.  [ 'Select', undef, $d->get('_Select') ],
  532.  [
  533.   'Select All',                'gtk-select-all',
  534.   $d->get('_All'),             '<control>a',
  535.   $d->get('Select all pages'), \&select_all
  536.  ],
  537.  [
  538.   'Select Odd', undef, $d->get('_Odd'), '<control>1',
  539.   $d->get('Select all odd-numbered pages'),
  540.   sub { select_odd_even(0); }
  541.  ],
  542.  [
  543.   'Select Even', undef, $d->get('_Even'), '<control>2',
  544.   $d->get('Select all evenly-numbered pages'),
  545.   sub { select_odd_even(1); }
  546.  ],
  547.  [
  548.   'Select Blank',                                      'gtk-select-blank',
  549.   $d->get('_Blank'),                                   '<control>b',
  550.   $d->get('Select pages with low standard deviation'), \&analyse_select_blank
  551.  ],
  552.  [
  553.   'Select Dark',                'gtk-select-blank',
  554.   $d->get('_Dark'),             '<control>d',
  555.   $d->get('Select dark pages'), \&analyse_select_dark
  556.  ],
  557.  [
  558.   'Select Modified',
  559.   'gtk-select-modified',
  560.   $d->get('_Modified'),
  561.   '<control>m',
  562.   $d->get('Select modified pages since last OCR'),
  563.   \&select_modified_since_ocr
  564.  ],
  565.  [
  566.   'Select No OCR',                            undef,
  567.   $d->get('_No OCR'),                         undef,
  568.   $d->get('Select pages with no OCR output'), \&select_no_ocr
  569.  ],
  570.  [
  571.   'Clear OCR',                                     'gtk-clear',
  572.   $d->get('_Clear OCR'),                           undef,
  573.   $d->get('Clear OCR output from selected pages'), \&clear_ocr
  574.  ],
  575.  [
  576.   'Properties',                     'gtk-properties',
  577.   $d->get('Propert_ies'),           undef,
  578.   $d->get('Edit image properties'), \&properties
  579.  ],
  580.  [
  581.   'Preferences',               'gtk-preferences',
  582.   $d->get('Prefere_nces'),     undef,
  583.   $d->get('Edit preferences'), \&preferences
  584.  ],
  585.  
  586.  # View menu
  587.  [ 'View', undef, $d->get('_View') ],
  588.  [
  589.   'Zoom 100',            'gtk-zoom-100',
  590.   $d->get('Zoom _100%'), undef,
  591.   $d->get('Zoom to 100%'), sub { $view->set_zoom(1.0); }
  592.  ],
  593.  [
  594.   'Zoom to fit',           'gtk-zoom-fit',
  595.   $d->get('Zoom to _fit'), undef,
  596.   $d->get('Zoom to fit'), sub { $view->set_fitting(TRUE); }
  597.  ],
  598.  [
  599.   'Zoom in',           'gtk-zoom-in',
  600.   $d->get('Zoom _in'), 'plus',
  601.   $d->get('Zoom in'), sub { $view->zoom_in; }
  602.  ],
  603.  [
  604.   'Zoom out',           'gtk-zoom-out',
  605.   $d->get('Zoom _out'), 'minus',
  606.   $d->get('Zoom out'), sub { $view->zoom_out; }
  607.  ],
  608.  [
  609.   'Rotate 90',
  610.   'rotate90',
  611.   $d->get('Rotate 90 clockwise'),
  612.   '<control><shift>R',
  613.   $d->get('Rotate 90 clockwise'),
  614.   sub { rotate( 90, [ indices2pages( $slist->get_selected_indices ) ] ) }
  615.  ],
  616.  [
  617.   'Rotate 180',
  618.   'rotate180',
  619.   $d->get('Rotate 180'),
  620.   '<control><shift>F',
  621.   $d->get('Rotate 180'),
  622.   sub { rotate( 180, [ indices2pages( $slist->get_selected_indices ) ] ) }
  623.  ],
  624.  [
  625.   'Rotate 270',
  626.   'rotate270',
  627.   $d->get('Rotate 90 anticlockwise'),
  628.   '<control><shift>C',
  629.   $d->get('Rotate 90 anticlockwise'),
  630.   sub { rotate( 270, [ indices2pages( $slist->get_selected_indices ) ] ) }
  631.  ],
  632.  
  633.  # Tools menu
  634.  [ 'Tools', undef, $d->get('_Tools') ],
  635.  [
  636.   'Threshold', undef, $d->get('_Threshold'), undef,
  637.   $d->get('Change each pixel above this threshold to black'),
  638.   \&threshold
  639.  ],
  640.  [
  641.   'Negate', undef, $d->get('_Negate'), undef,
  642.   $d->get('Converts black to white and vice versa'), \&negate
  643.  ],
  644.  [
  645.   'Unsharp',                        undef,
  646.   $d->get('_Unsharp Mask'),         undef,
  647.   $d->get('Apply an unsharp mask'), \&unsharp
  648.  ],
  649.  [
  650.   'Crop', 'GTK_STOCK_LEAVE_FULLSCREEN', $d->get('_Crop'), undef,
  651.   $d->get('Crop pages'), \&crop
  652.  ],
  653.  [
  654.   'unpaper',                                       undef,
  655.   $d->get('_Clean up'),                            undef,
  656.   $d->get('Clean up scanned images with unpaper'), \&unpaper
  657.  ],
  658.  [
  659.   'OCR', undef, $d->get('_OCR'), undef,
  660.   $d->get('Optical Character Recognition'), \&OCR
  661.  ],
  662.  [ 'User-defined', undef, $d->get('U_ser-defined') ],
  663.  
  664.  # Help menu
  665.  [ 'Help menu', undef, $d->get('_Help') ],
  666.  [
  667.   'Help',          'gtk-help', $d->get('_Help'), '<control>h',
  668.   $d->get('Help'), \&view_pod
  669.  ],
  670.  [ 'About', 'gtk-about', $d->get('_About'), undef, $d->get('_About'), \&about ],
  671. );
  672.  
  673. my @image_tools = (
  674.  [
  675.   'DraggerTool',                'gtk-refresh',
  676.   $d->get('_Drag'),             undef,
  677.   $d->get('Use the hand tool'), 10
  678.  ],
  679.  [
  680.   'SelectorTool',                                'selection',
  681.   $d->get('_Select'),                            undef,
  682.   $d->get('Use the rectangular selection tool'), 20
  683.  ],
  684.  [
  685.   'PainterTool',                   'gtk-media-play',
  686.   $d->get('_Paint'),               undef,
  687.   $d->get('Use the painter tool'), 30
  688.  ]
  689. );
  690.  
  691. # Declare the XML structure
  692. my $uimanager;
  693. my $ui = <<'EOS';
  694. <ui>
  695.  <menubar name='MenuBar'>
  696.   <menu action='File'>
  697.    <menuitem action='New'/>
  698.    <menuitem action='Open'/>
  699.    <menuitem action='Scan'/>
  700.    <menuitem action='Save'/>
  701.    <menuitem action='Email as PDF'/>
  702.    <menuitem action='Print'/>
  703.    <separator/>
  704.    <menuitem action='Compress'/>
  705.    <separator/>
  706.    <menuitem action='Quit'/>
  707.   </menu>
  708.   <menu action='Edit'>
  709.    <menuitem action='Undo'/>
  710.    <menuitem action='Redo'/>
  711.    <separator/>
  712.    <menuitem action='Cut'/>
  713.    <menuitem action='Copy'/>
  714.    <menuitem action='Paste'/>
  715.    <menuitem action='Delete'/>
  716.    <separator/>
  717.    <menuitem action='Renumber'/>
  718.    <menu action='Select'>
  719.     <menuitem action='Select All'/>
  720.     <menuitem action='Select Odd'/>
  721.     <menuitem action='Select Even'/>
  722.     <menuitem action='Select Blank'/>
  723.     <menuitem action='Select Dark'/>
  724.     <menuitem action='Select Modified'/>
  725.     <menuitem action='Select No OCR'/>
  726.    </menu>
  727.    <menuitem action='Clear OCR'/>
  728.    <separator/>
  729.    <menuitem action='Properties'/>
  730.    <separator/>
  731.    <menuitem action='Preferences'/>
  732.   </menu>
  733.   <menu action='View'>
  734.    <menuitem action = 'DraggerTool'/>
  735.    <menuitem action = 'SelectorTool'/>
  736.    <separator/>
  737.    <menuitem action='Zoom 100'/>
  738.    <menuitem action='Zoom to fit'/>
  739.    <menuitem action='Zoom in'/>
  740.    <menuitem action='Zoom out'/>
  741.    <separator/>
  742.    <menuitem action='Rotate 90'/>
  743.    <menuitem action='Rotate 180'/>
  744.    <menuitem action='Rotate 270'/>
  745.   </menu>
  746.   <menu action='Tools'>
  747.    <menuitem action='Threshold'/>
  748.    <menuitem action='Negate'/>
  749.    <menuitem action='Unsharp'/>
  750.    <menuitem action='Crop'/>
  751.    <separator/>
  752.    <menuitem action='unpaper'/>
  753.    <menuitem action='OCR'/>
  754.    <separator/>
  755.    <menu action='User-defined'>
  756.    </menu>
  757.   </menu>
  758.   <menu action='Help menu'>
  759.    <menuitem action='Help'/>
  760.    <menuitem action='About'/>
  761.   </menu>
  762.  </menubar>
  763.  <toolbar name='ToolBar'>
  764.   <toolitem action='New'/>
  765.   <toolitem action='Open'/>
  766.   <toolitem action='Scan'/>
  767.   <toolitem action='Save'/>
  768.   <toolitem action='Email as PDF'/>
  769.   <toolitem action='Print'/>
  770.   <separator/>
  771.   <toolitem action='Undo'/>
  772.   <toolitem action='Redo'/>
  773.   <separator/>
  774.   <toolitem action='Cut'/>
  775.   <toolitem action='Copy'/>
  776.   <toolitem action='Paste'/>
  777.   <toolitem action='Delete'/>
  778.   <separator/>
  779.   <toolitem action='Renumber'/>
  780.   <toolitem action='Select All'/>
  781.   <separator/>
  782.   <toolitem action = 'DraggerTool'/>
  783.   <toolitem action = 'SelectorTool'/>
  784.   <separator/>
  785.   <toolitem action='Zoom 100'/>
  786.   <toolitem action='Zoom to fit'/>
  787.   <toolitem action='Zoom in'/>
  788.   <toolitem action='Zoom out'/>
  789.   <separator/>
  790.   <toolitem action='Rotate 90'/>
  791.   <toolitem action='Rotate 180'/>
  792.   <toolitem action='Rotate 270'/>
  793.   <separator/>
  794.   <toolitem action='Help'/>
  795.   <toolitem action='Quit'/>
  796.  </toolbar>
  797.  <popup name='Detail_Popup'>
  798.   <menuitem action = 'DraggerTool'/>
  799.   <menuitem action = 'SelectorTool'/>
  800.   <separator/>
  801.   <menuitem action='Zoom 100'/>
  802.   <menuitem action='Zoom to fit'/>
  803.   <menuitem action='Zoom in'/>
  804.   <menuitem action='Zoom out'/>
  805.   <separator/>
  806.   <menuitem action='Rotate 90'/>
  807.   <menuitem action='Rotate 180'/>
  808.   <menuitem action='Rotate 270'/>
  809.   <separator/>
  810.   <menuitem action='Cut'/>
  811.   <menuitem action='Copy'/>
  812.   <menuitem action='Paste'/>
  813.   <menuitem action='Delete'/>
  814.   <separator/>
  815.   <menuitem action='Properties'/>
  816.  </popup>
  817.  <popup name='Thumb_Popup'>
  818.   <menuitem action='Save'/>
  819.   <menuitem action='Email as PDF'/>
  820.   <menuitem action='Print'/>
  821.   <separator/>
  822.   <menuitem action='Renumber'/>
  823.   <menuitem action='Select All'/>
  824.   <menuitem action='Select Odd'/>
  825.   <menuitem action='Select Even'/>
  826.   <separator/>
  827.   <menuitem action='Rotate 90'/>
  828.   <menuitem action='Rotate 180'/>
  829.   <menuitem action='Rotate 270'/>
  830.   <separator/>
  831.   <menuitem action='Cut'/>
  832.   <menuitem action='Copy'/>
  833.   <menuitem action='Paste'/>
  834.   <menuitem action='Delete'/>
  835.   <separator/>
  836.   <menuitem action='Clear OCR'/>
  837.   <separator/>
  838.   <menuitem action='Properties'/>
  839.  </popup>
  840. </ui>
  841. EOS
  842.  
  843. # Create the window
  844. my $window = Gtk2::Window->new;
  845. $window->set_title("$prog_name v$version");
  846. $window->signal_connect(
  847.  'delete-event' => sub {
  848.   if ( quit() ) {
  849.    Gtk2->main_quit;
  850.   }
  851.   else {
  852.    return TRUE;
  853.   }
  854.  }
  855. );
  856.  
  857. # Note when the window is maximised or not.
  858. $window->signal_connect(
  859.  window_state_event => sub {
  860.   my ( $w, $event ) = @_;
  861.   if ( $event->new_window_state & ['maximized'] ) {
  862.    $SETTING{'window_maximize'} = TRUE;
  863.   }
  864.   else {
  865.    $SETTING{'window_maximize'} = FALSE;
  866.   }
  867.  }
  868. );
  869.  
  870. # If defined in the config file, set the window state, size and position
  871. if ( $SETTING{'restore window'} ) {
  872.  $window->set_default_size( $SETTING{'window_width'},
  873.   $SETTING{'window_height'} );
  874.  $window->move( $SETTING{'window_x'}, $SETTING{'window_y'} )
  875.    if ( defined( $SETTING{'window_x'} ) and defined( $SETTING{'window_y'} ) );
  876.  $window->maximize if ( $SETTING{'window_maximize'} );
  877. }
  878.  
  879. try { $window->set_default_icon_from_file("$iconpath/gscan2pdf.svg"); }
  880. catch {
  881.  $logger->warn("Unable to load icon `$iconpath/gscan2pdf.svg': $@");
  882. };
  883.  
  884. my $main_vbox = Gtk2::VBox->new;
  885. $window->add($main_vbox);
  886.  
  887. # Create the menu bar
  888. create_menu_bar();
  889. $main_vbox->pack_start( $menubar, FALSE, TRUE,  0 );
  890. $main_vbox->pack_start( $toolbar, FALSE, FALSE, 0 );
  891.  
  892. my $tooltips = Gtk2::Tooltips->new;
  893. $tooltips->enable;
  894.  
  895. # HPaned for thumbnails and detail view
  896. my $hpaned = Gtk2::HPaned->new;
  897. $hpaned->set_position( $SETTING{'thumb panel'} );
  898. $main_vbox->pack_start( $hpaned, TRUE, TRUE, 0 );
  899.  
  900. # Scrolled window for thumbnails
  901. my $scwin_thumbs = Gtk2::ScrolledWindow->new;
  902.  
  903. # resize = FALSE to stop the panel expanding on being resized (Debian #507032)
  904. $hpaned->pack1( $scwin_thumbs, FALSE, TRUE );
  905. $scwin_thumbs->set_policy( 'automatic', 'automatic' );
  906. $scwin_thumbs->set_shadow_type('etched-in');
  907.  
  908. # Set up a SimpleList
  909. $slist = Gscan2pdf::Document->new;
  910.  
  911. my $target_entry = {
  912.  target => 'Glib::Scalar',    # some string representing the drag type
  913.  flags  => 'same-widget',     # Gtk2::TargetFlags
  914.  info   => 1,                 # some app-defined integer identifier
  915. };
  916. $slist->drag_source_set( 'button1-mask', [ 'copy', 'move' ], $target_entry );
  917. $slist->drag_dest_set(
  918.  [ 'motion', 'highlight' ],
  919.  [ 'copy',   'move' ],
  920.  $target_entry
  921. );
  922.  
  923. $slist->signal_connect(
  924.  'drag-data-get' => sub {
  925.   my ( $tree, $context, $sel ) = @_;
  926.   $sel->set( $sel->target, 8, 'data' );
  927.  }
  928. );
  929.  
  930. $slist->signal_connect(
  931.  'drag-data-delete' => sub {
  932.   my ( $tree, $context ) = @_;
  933.   my $model = $tree->get_model;
  934.   my @data  = $tree->get_selection->get_selected_rows;
  935.  
  936.   for ( reverse @data ) {
  937.    my $iter = $model->get_iter($_);
  938.    my $info = $model->get( $iter, 0 );
  939.    $model->remove($iter);
  940.   }
  941.  
  942.   $tree->get_selection->unselect_all;
  943.  }
  944. );
  945.  
  946. $slist->signal_connect(
  947.  'drag-data-received' => sub {
  948.   my ( $tree, $context, $x, $y, $sel ) = @_;
  949.   my ( $path, $how ) = $tree->get_dest_row_at_pos( $x, $y );
  950.   my $model = $tree->get_model;
  951.   my $data = $sel->data or return;
  952.   my $delete =
  953.     $context->action == 'move';    ## no critic (ProhibitMismatchedOperators)
  954.  
  955.   my @rows = $tree->get_selection->get_selected_rows or return;
  956.   my @data;
  957.   for (@rows) {
  958.    my $iter = $model->get_iter($_);
  959.    my @info = $model->get($iter);
  960.    my $suffix;
  961.    $suffix = $1 if ( $info[2]->{filename} =~ /(\.\w*)$/x );
  962.    my $new = File::Temp->new( DIR => $SETTING{session}, SUFFIX => $suffix );
  963.  
  964.    # quotes required to prevent File::Temp objects from being clobbered
  965.    copy( "$info[2]->{filename}", "$new" )
  966.      or show_message_dialog( $window, 'error', 'close',
  967.     $d->get('Error copying page') );
  968.  
  969.    $info[2]->{filename} = $new;
  970.    push @data, [@info];
  971.   }
  972.  
  973.   # Block row-changed signal so that the list can be updated before the sort
  974.   # takes over.
  975.   $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  976.  
  977.   if ($path) {
  978.    if ( $how eq 'after' or $how eq 'into-or-after' ) {
  979.     splice @{ $slist->{data} }, $path->to_string + 1, 0, @data;
  980.    }
  981.    else {
  982.     splice @{ $slist->{data} }, $path->to_string, 0, @data;
  983.    }
  984.   }
  985.   else {
  986.    push @{ $slist->{data} }, @data;
  987.   }
  988.   $slist->renumber;
  989.   $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  990.  
  991.   # Update the start spinbutton if necessary
  992.   update_start();
  993.  
  994.   $context->finish( 1, $delete, time );
  995.  }
  996. );
  997.  
  998. # Callback for dropped signal.
  999. $slist->signal_connect(
  1000.  drag_drop => sub {
  1001.   my ( $tree, $context, $x, $y, $when ) = @_;
  1002.   if ( my $targ = $context->targets ) {
  1003.    $tree->drag_get_data( $context, $targ, $when );
  1004.    return TRUE;
  1005.   }
  1006.   return FALSE;
  1007.  }
  1008. );
  1009.  
  1010. # If dragged below the bottom of the window, scroll it.
  1011. $slist->signal_connect(
  1012.  'drag-motion' => sub {
  1013.   my ( $tree, $context, $x, $y, $t ) = @_;
  1014.   my ( $path, $how ) = $tree->get_dest_row_at_pos( $x, $y ) or return;
  1015.   my $scroll = $tree->parent;
  1016.  
  1017.   # Add the marker showing the drop in the tree
  1018.   $tree->set_drag_dest_row( $path, $how );
  1019.  
  1020.   # Make move the default
  1021.   my @action;
  1022.   if ( $context->actions == 'copy' )  ## no critic (ProhibitMismatchedOperators)
  1023.   {
  1024.    @action = ('copy');
  1025.   }
  1026.   else {
  1027.    @action = ('move');
  1028.   }
  1029.   $context->status( @action, $t );
  1030.  
  1031.   my $adj = $scroll->get_vadjustment;
  1032.   my ( $value, $step ) = ( $adj->value, $adj->step_increment );
  1033.  
  1034.   if ( $y > $adj->page_size - $step / 2 ) {
  1035.    my $v = $value + $step;
  1036.    my $m = $adj->upper - $adj->page_size;
  1037.    $adj->set_value( $v > $m ? $m : $v );
  1038.   }
  1039.   elsif ( $y < $step / 2 ) {
  1040.    my $v = $value - $step;
  1041.    my $m = $adj->lower;
  1042.    $adj->set_value( $v < $m ? $m : $v );
  1043.   }
  1044.  
  1045.   return FALSE;
  1046.  }
  1047. );
  1048.  
  1049. # Set up callback for right mouse clicks.
  1050. $slist->signal_connect( button_press_event   => \&handle_clicks );
  1051. $slist->signal_connect( button_release_event => \&handle_clicks );
  1052.  
  1053. # Set the page number to be editable
  1054. $slist->set_column_editable( 0, TRUE );
  1055.  
  1056. # Set-up the callback when the page number has been edited.
  1057. $slist->{row_changed_signal} = $slist->get_model->signal_connect(
  1058.  'row-changed' => sub {
  1059.   $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  1060.  
  1061.   # Sort pages
  1062.   $slist->manual_sort_by_column(0);
  1063.  
  1064.   # And make sure there are no duplicates
  1065.   $slist->renumber;
  1066.   $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  1067.  
  1068.   # Update the start spinbutton if necessary
  1069.   update_start();
  1070.  }
  1071. );
  1072.  
  1073. $scwin_thumbs->add($slist);
  1074.  
  1075. # Notebook for detail view and OCR output
  1076. my $vnotebook = Gtk2::Notebook->new;
  1077. $hpaned->pack2( $vnotebook, TRUE, TRUE );
  1078.  
  1079. # ImageView for detail view
  1080. $view = Gtk2::ImageView->new;
  1081. my $dragger  = Gtk2::ImageView::Tool::Dragger->new($view);
  1082. my $selector = Gtk2::ImageView::Tool::Selector->new($view);
  1083. my $painter  = Gtk2::ImageView::Tool::Painter->new($view);
  1084. $vnotebook->append_page( $view, $d->get('Image') );
  1085.  
  1086. $view->signal_connect(
  1087.  button_press_event => sub {
  1088.   my ( $widget, $event ) = @_;
  1089.   handle_clicks( $widget, $event );
  1090.  }
  1091. );
  1092. $view->signal_connect( button_release_event => \&handle_clicks );
  1093. $view->signal_connect(
  1094.  'zoom-changed' => sub {
  1095.   $canvas->set_scale( $view->get_zoom ) if ( defined $canvas );
  1096.  }
  1097. );
  1098.  
  1099. # Callback if the selection changes
  1100. $selector->{selection_changed_signal} = $selector->signal_connect(
  1101.  'selection-changed' => sub {
  1102.   my $sel = $selector->get_selection;
  1103.   if ( defined $sel ) {
  1104.    $SETTING{selection} = [ $sel->values ];
  1105.    if ( defined $sb_selector_x ) {
  1106.     $sb_selector_x->set_value( $SETTING{selection}[0] );
  1107.     $sb_selector_y->set_value( $SETTING{selection}[1] );
  1108.     $sb_selector_w->set_value( $SETTING{selection}[2] );
  1109.     $sb_selector_h->set_value( $SETTING{selection}[3] );
  1110.    }
  1111.   }
  1112.  }
  1113. );
  1114.  
  1115. # ScrolledWindow for Goo::Canvas for OCR output
  1116. my $scwin_buffer = Gtk2::ScrolledWindow->new;
  1117. $scwin_buffer->set_policy( 'automatic', 'automatic' );
  1118. $scwin_buffer->set_shadow_type('etched-in');
  1119. $vnotebook->append_page( $scwin_buffer, $d->get('OCR Output') );
  1120.  
  1121. # Set up call back for list selection to update detail view
  1122. $slist->{selection_changed_signal} = $slist->get_selection->signal_connect(
  1123.  changed => sub {
  1124.   my @page = $slist->get_selected_indices;
  1125.  
  1126.   # Display the new image
  1127.   $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
  1128.   if (@page) {
  1129.  
  1130.    my $path = Gtk2::TreePath->new_from_indices( $page[0] );
  1131.    $slist->scroll_to_cell($path);
  1132.    my $sel = $selector->get_selection;
  1133.    display_image( $slist->{data}[ $page[0] ][2] );
  1134.    $selector->set_selection($sel) if ( defined $sel );
  1135.   }
  1136.   else {
  1137.    $view->set_pixbuf(undef);
  1138.    undef $current_page;
  1139.   }
  1140.  
  1141.   # If no pages are selected, but some exist, PageRange widgets default to all
  1142.   if ( $#page == -1 and $#{ $slist->{data} } > -1 ) {
  1143.    $SETTING{'Page range'} = 'all';
  1144.    foreach (@prlist) {
  1145.     $_->set_active( $SETTING{'Page range'} );
  1146.    }
  1147.   }
  1148.  }
  1149. );
  1150.  
  1151. # _after ensures that Editables get first bite
  1152. $window->signal_connect_after(
  1153.  key_press_event => sub {
  1154.   my ( $widget, $event ) = @_;
  1155.  
  1156.   # Let the keypress propagate
  1157.   return FALSE
  1158.     unless ( $event->keyval ==
  1159.    $Gtk2::Gdk::Keysyms{Delete} )    ## no critic (ProhibitPackageVars)
  1160.     ;
  1161.  
  1162.   delete_pages();
  1163.   return TRUE;
  1164.  }
  1165. );
  1166.  
  1167. # If defined in the config file, set the current directory
  1168. $SETTING{'cwd'} = getcwd unless ( defined( $SETTING{'cwd'} ) );
  1169.  
  1170. my $unpaper = Gscan2pdf::Unpaper->new( $SETTING{'unpaper options'} );
  1171.  
  1172. # Set up the strings for the possible device-dependent options
  1173. my %ddo;
  1174. my %pddo = (
  1175.  'mode' => {
  1176.   string => $d->get('Mode'),
  1177.   values => {
  1178.    'Lineart'       => $d->get('Lineart'),
  1179.    'Grayscale',    => $d->get('Grayscale'),
  1180.    'Gray'          => $d->get('Gray'),
  1181.    'Color'         => $d->get('Color'),
  1182.    'Black & White' => $d->get('Black & White'),
  1183.    'True Gray'     => $d->get('True Gray'),
  1184.    'Binary'        => $d->get('Binary'),
  1185.    'auto'          => $d->get('Auto'),
  1186.    'Halftone'      => $d->get('Halftone'),
  1187.    '24bit Color'   => $d->get('24bit Color'),
  1188.   }
  1189.  },
  1190.  'compression' => {
  1191.   string => $d->get('Compression'),
  1192.   values => {
  1193.    'None' => $d->get('None'),
  1194.    'JPEG' => $d->get('JPEG'),
  1195.   },
  1196.  },
  1197.  'resolution' => { string => $d->get('Resolution') },
  1198.  'brightness' => { string => $d->get('Brightness') },
  1199.  'gain'       => { string => $d->get('Gain') },
  1200.  'contrast'   => { string => $d->get('Contrast') },
  1201.  'threshold'  => { string => $d->get('Threshold') },
  1202.  'speed'      => {
  1203.   string => $d->get('Speed'),
  1204.   values => {
  1205.    'yes' => $d->get('Yes'),
  1206.    'no'  => $d->get('No'),
  1207.   },
  1208.  },
  1209.  'batch-scan' => {
  1210.   string => $d->get('Batch scan'),
  1211.   values => {
  1212.    'yes' => $d->get('Yes'),
  1213.    'no'  => $d->get('No'),
  1214.   },
  1215.  },
  1216.  'wait-for-button' => {
  1217.   string => $d->get('Wait for button'),
  1218.   values => {
  1219.    'yes' => $d->get('Yes'),
  1220.    'no'  => $d->get('No'),
  1221.   },
  1222.  },
  1223.  'button-wait' => {
  1224.   string => $d->get('Wait for button'),
  1225.   values => {
  1226.    'yes' => $d->get('Yes'),
  1227.    'no'  => $d->get('No'),
  1228.   },
  1229.  },
  1230.  'calibration-cache' => {
  1231.   string => $d->get('Cache calibration'),
  1232.   values => {
  1233.    'yes' => $d->get('Yes'),
  1234.    'no'  => $d->get('No'),
  1235.   },
  1236.  },
  1237.  'source' => {
  1238.   string => $d->get('Source'),
  1239.   values => {
  1240.    'Normal'                    => $d->get('Normal'),
  1241.    'ADF'                       => $d->get('ADF'),
  1242.    'Automatic Document Feeder' => $d->get('Automatic Document Feeder'),
  1243.    'XPA'                       => $d->get('XPA'),
  1244.    'auto'                      => $d->get('Auto'),
  1245.    'Auto'                      => $d->get('Auto'),
  1246.    'Flatbed'                   => $d->get('Flatbed'),
  1247.    'Transparency Adapter'      => $d->get('Transparency Adapter'),
  1248.    'Transparency Unit'         => $d->get('Transparency Unit'),
  1249.   },
  1250.  },
  1251.  'pagewidth'       => { string => $d->get('Page Width') },
  1252.  'pageheight'      => { string => $d->get('Page Height') },
  1253.  'page-width'      => { string => $d->get('Page Width') },
  1254.  'page-height'     => { string => $d->get('Page Height') },
  1255.  'overscan-top'    => { string => $d->get('Top Overscan') },
  1256.  'overscan-bottom' => { string => $d->get('Bottom Overscan') },
  1257.  'adf_mode'        => {
  1258.   string => $d->get('ADF Mode'),
  1259.   values => {
  1260.    'Simplex' => $d->get('Simplex'),
  1261.    'Duplex'  => $d->get('Duplex'),
  1262.   },
  1263.  },
  1264.  'adf-mode' => {
  1265.   string => $d->get('ADF Mode'),
  1266.   values => {
  1267.    'Simplex' => $d->get('Simplex'),
  1268.    'Duplex'  => $d->get('Duplex'),
  1269.   },
  1270.  },
  1271. );
  1272.  
  1273. update_uimanager();
  1274.  
  1275. # Look for crashed sessions
  1276. if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
  1277.  $tmpdir = $SETTING{TMPDIR};
  1278. }
  1279. else {
  1280.  $tmpdir = File::Spec->tmpdir;
  1281. }
  1282. $logger->info("Checking $tmpdir for crashed sessions");
  1283. my ( @sessions, @crashed, $selected ) =
  1284.   glob File::Spec->catfile( $tmpdir, 'gscan2pdf-????' );
  1285.  
  1286. # Forget those used by running sessions
  1287. for (@sessions) {
  1288.  if (
  1289.   open $lockfh,
  1290.   '>',
  1291.   File::Spec->catfile( $_, 'lockfile' ) and flock( $lockfh, LOCK_EX | LOCK_NB )
  1292.    )
  1293.  {
  1294.   push @crashed, $_;
  1295.   flock( $lockfh, LOCK_UN );
  1296.   close $lockfh;
  1297.  }
  1298. }
  1299.  
  1300. # Allow user to pick a crashed session to restore
  1301. if (@crashed) {
  1302.  my $dialog = Gtk2::Dialog->new(
  1303.   $d->get('Pick crashed session to restore'),
  1304.   $window, 'modal',
  1305.   'gtk-ok'     => 'ok',
  1306.   'gtk-cancel' => 'cancel'
  1307.  );
  1308.  my $label = Gtk2::Label->new( $d->get('Pick crashed session to restore') );
  1309.  $dialog->vbox->add($label);
  1310.  my $sessionlist = Gtk2::Ex::Simple::List->new( $d->get('Session') => 'text', );
  1311.  push @{ $sessionlist->{data} }, @crashed;
  1312.  $dialog->vbox->add($sessionlist);
  1313.  $dialog->show_all;
  1314.  ($selected) = $sessionlist->get_selected_indices if ( $dialog->run eq 'ok' );
  1315.  $dialog->destroy;
  1316.  
  1317.  if ( defined $selected ) {
  1318.   $session = $crashed[$selected];
  1319.   open $lockfh, '>',    ## no critic (RequireBriefOpen)
  1320.     File::Spec->catfile( $session, 'lockfile' )
  1321.     or die "Cannot open lockfile\n";
  1322.   flock( $lockfh, LOCK_EX ) or die "Cannot lock file\n";
  1323.   open_session();
  1324.  }
  1325. }
  1326.  
  1327. # Create temporary directory if necessary
  1328. unless ( defined $session ) {
  1329.  if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
  1330.   mkdir( $SETTING{TMPDIR} ) if ( not -d $SETTING{TMPDIR} );
  1331.   try {
  1332.    $session = File::Temp->newdir( 'gscan2pdf-XXXX', DIR => $SETTING{TMPDIR} );
  1333.   }
  1334.   catch {
  1335.    $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
  1336.    $slist->set_dir($session);
  1337.    $logger->warn(
  1338.     sprintf(
  1339.      $d->get(
  1340. "Warning: unable to use %s for temporary storage. Defaulting to %s instead."
  1341.      ),
  1342.      $SETTING{TMPDIR},
  1343.      dirname($session)
  1344.     )
  1345.    );
  1346.   };
  1347.  }
  1348.  else {
  1349.   $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
  1350.   $slist->set_dir($session);
  1351.  }
  1352.  open $lockfh, '>',    ## no critic (RequireBriefOpen)
  1353.    File::Spec->catfile( $session, 'lockfile' )
  1354.    or die "Cannot open lockfile\n";
  1355.  flock( $lockfh, LOCK_EX ) or die "Cannot lock file\n";
  1356.  $slist->save_session($session);
  1357.  $logger->info("Using $session for temporary files");
  1358. }
  1359.  
  1360. $window->show_all;
  1361.  
  1362. # Progress bars below window
  1363. my $phbox = Gtk2::HBox->new;
  1364. $main_vbox->pack_end( $phbox, FALSE, FALSE, 0 );
  1365. $phbox->show;
  1366. my $shbox = Gtk2::HBox->new;
  1367. $phbox->add($shbox);
  1368. my $spbar = Gtk2::ProgressBar->new;
  1369. $shbox->add($spbar);
  1370. my $scbutton = Gtk2::Button->new;
  1371. $scbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
  1372. $shbox->pack_end( $scbutton, FALSE, FALSE, 0 );
  1373. my $thbox = Gtk2::HBox->new;
  1374. $phbox->add($thbox);
  1375. my $tpbar = Gtk2::ProgressBar->new;
  1376. $thbox->add($tpbar);
  1377. my $tcbutton = Gtk2::Button->new;
  1378. $tcbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
  1379. $thbox->pack_end( $tcbutton, FALSE, FALSE, 0 );
  1380.  
  1381. ### Subroutines
  1382.  
  1383. # Create the menu bar, initialize its menus, and return the menu bar.
  1384.  
  1385. sub create_menu_bar {
  1386.  
  1387.  # Create a Gtk2::UIManager instance
  1388.  $uimanager = Gtk2::UIManager->new;
  1389.  
  1390.  # extract the accelgroup and add it to the window
  1391.  my $accelgroup = $uimanager->get_accel_group;
  1392.  $window->add_accel_group($accelgroup);
  1393.  
  1394.  # Create the basic Gtk2::ActionGroup instance
  1395.  # and fill it with Gtk2::Action instances
  1396.  my $actions_basic = Gtk2::ActionGroup->new("actions_basic");
  1397.  $actions_basic->add_actions( \@action_items, undef );
  1398.  $actions_basic->add_radio_actions( \@image_tools, 10, \&change_image_tool_cb );
  1399.  
  1400.  # Add the actiongroup to the uimanager
  1401.  $uimanager->insert_action_group( $actions_basic, 0 );
  1402.  
  1403.  # add the basic XML description of the GUI
  1404.  $uimanager->add_ui_from_string($ui);
  1405.  
  1406.  # extract the menubar
  1407.  $menubar = $uimanager->get_widget('/MenuBar');
  1408.  
  1409.  $SETTING{user_defined_tools} = [ $SETTING{user_defined_tools} ]
  1410.    if ( ref( $SETTING{user_defined_tools} ) ne 'ARRAY' );
  1411.  for ( @{ $SETTING{user_defined_tools} } ) {
  1412.   my %tool = ( cmd => $_ );
  1413.   push @user_defined_tools, \%tool;
  1414.   add_user_defined_tool_menu( \%tool );
  1415.  }
  1416.  
  1417.  # Check for presence of various packages
  1418.  $dependencies{perlmagick}  = try { require Image::Magick };
  1419.  $dependencies{imagemagick} = check_command('convert');
  1420.  $dependencies{scanadf}     = check_command('scanadf');
  1421.  $dependencies{xdg}         = check_command('xdg-email');
  1422.  $dependencies{gocr}        = check_command('gocr');
  1423.  $dependencies{tesseract}   = Gscan2pdf::Tesseract->setup($logger);
  1424.  $dependencies{ocropus}     = Gscan2pdf::Ocropus->setup($logger);
  1425.  $dependencies{cuneiform}   = Gscan2pdf::Cuneiform->setup($logger);
  1426.  $dependencies{djvu}        = check_command('cjb2');
  1427.  $dependencies{unpaper}     = Gscan2pdf::Unpaper->version;
  1428.  $dependencies{libtiff}     = check_command('tiffcp');
  1429.  $logger->info("Found Image::Magick") if ( $dependencies{perlmagick} );
  1430.  $logger->info("Found ImageMagick")   if ( $dependencies{imagemagick} );
  1431.  $logger->info("Found scanadf")       if ( $dependencies{scanadf} );
  1432.  $logger->info("Found xdg-email")     if ( $dependencies{xdg} );
  1433.  $logger->info("Found gocr")          if ( $dependencies{gocr} );
  1434.  $logger->info("Found tesseract")     if ( $dependencies{tesseract} );
  1435.  $logger->info("Found cuneiform v$dependencies{cuneiform}")
  1436.    if ( $dependencies{cuneiform} );
  1437.  $logger->info("Found cjb2 (djvu)") if ( $dependencies{djvu} );
  1438.  $logger->info("Found libtiff")     if ( $dependencies{libtiff} );
  1439.  $logger->info("Found unpaper $dependencies{unpaper}")
  1440.    if ( $dependencies{unpaper} );
  1441.  
  1442.  # OCR engine options
  1443.  push @ocr_engine,
  1444.    [ 'gocr', $d->get('GOCR'), $d->get('Process image with GOCR.') ]
  1445.    if ( $dependencies{gocr} );
  1446.  push @ocr_engine,
  1447.    [
  1448.   'tesseract', $d->get('Tesseract'),
  1449.   $d->get('Process image with Tesseract.')
  1450.    ],
  1451.    if ( $dependencies{tesseract} );
  1452.  push @ocr_engine,
  1453.    [ 'ocropus', $d->get('Ocropus'), $d->get('Process image with Ocropus.') ],
  1454.    if ( $dependencies{ocropus} );
  1455.  push @ocr_engine,
  1456.    [
  1457.   'cuneiform', $d->get('Cuneiform'),
  1458.   $d->get('Process image with Cuneiform.')
  1459.    ],
  1460.    if ( $dependencies{cuneiform} );
  1461.  
  1462.  # Ghost save image item if imagemagick not available
  1463.  my $msg = '';
  1464.  $msg .= $d->get("Save image and Save as PDF both require imagemagick\n")
  1465.    unless ( $dependencies{imagemagick} );
  1466.  
  1467.  # Ghost save image item if libtiff not available
  1468.  $msg .= $d->get("Save image requires libtiff\n")
  1469.    unless ( $dependencies{libtiff} );
  1470.  
  1471.  # Ghost djvu item if cjb2 not available
  1472.  $msg .= $d->get("Save as DjVu requires djvulibre-bin\n")
  1473.    unless ( $dependencies{djvu} );
  1474.  
  1475.  # Ghost email item if xdg-email not available
  1476.  $msg .= $d->get("Email as PDF requires xdg-email\n")
  1477.    unless ( $dependencies{xdg} );
  1478.  
  1479.  # Undo/redo start off ghosted anyway-
  1480.  $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
  1481.  $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(FALSE);
  1482.  $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);
  1483.  $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(FALSE);
  1484.  
  1485.  # save * start off ghosted anyway-
  1486.  $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(FALSE);
  1487.  $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(FALSE);
  1488.  $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(FALSE);
  1489.  $uimanager->get_widget('/ToolBar/Save')->set_sensitive(FALSE);
  1490.  $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(FALSE);
  1491.  $uimanager->get_widget('/ToolBar/Print')->set_sensitive(FALSE);
  1492.  $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(FALSE);
  1493.  $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(FALSE);
  1494.  $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(FALSE);
  1495.  
  1496.  $uimanager->get_widget('/MenuBar/Tools/Threshold')->set_sensitive(FALSE);
  1497.  $uimanager->get_widget('/MenuBar/Tools/Negate')->set_sensitive(FALSE);
  1498.  $uimanager->get_widget('/MenuBar/Tools/Unsharp')->set_sensitive(FALSE);
  1499.  $uimanager->get_widget('/MenuBar/Tools/Crop')->set_sensitive(FALSE);
  1500.  $uimanager->get_widget('/MenuBar/Tools/User-defined')->set_sensitive(FALSE);
  1501.  
  1502.  # a convenient place to put these
  1503.  $dependencies{pages} = -1;
  1504.  
  1505.  # Ghost rotations and unpaper if perlmagick not available
  1506.  unless ( $dependencies{perlmagick} ) {
  1507.   $uimanager->get_widget('/MenuBar/View/Rotate 90')->set_sensitive(FALSE);
  1508.   $uimanager->get_widget('/MenuBar/View/Rotate 180')->set_sensitive(FALSE);
  1509.   $uimanager->get_widget('/MenuBar/View/Rotate 270')->set_sensitive(FALSE);
  1510.   $uimanager->get_widget('/ToolBar/Rotate 90')->set_sensitive(FALSE);
  1511.   $uimanager->get_widget('/ToolBar/Rotate 180')->set_sensitive(FALSE);
  1512.   $uimanager->get_widget('/ToolBar/Rotate 270')->set_sensitive(FALSE);
  1513.   $uimanager->get_widget('/Detail_Popup/Rotate 90')->set_sensitive(FALSE);
  1514.   $uimanager->get_widget('/Detail_Popup/Rotate 180')->set_sensitive(FALSE);
  1515.   $uimanager->get_widget('/Detail_Popup/Rotate 270')->set_sensitive(FALSE);
  1516.   $uimanager->get_widget('/Thumb_Popup/Rotate 90')->set_sensitive(FALSE);
  1517.   $uimanager->get_widget('/Thumb_Popup/Rotate 180')->set_sensitive(FALSE);
  1518.   $uimanager->get_widget('/Thumb_Popup/Rotate 270')->set_sensitive(FALSE);
  1519.   $uimanager->get_widget('/MenuBar/Tools/unpaper')->set_sensitive(FALSE);
  1520.   $msg .=
  1521.     $d->get("The rotating options and unpaper support require perlmagick\n");
  1522.  }
  1523.  
  1524.  # Ghost unpaper item if unpaper not available
  1525.  unless ( $dependencies{unpaper} ) {
  1526.   $uimanager->get_widget('/MenuBar/Tools/unpaper')->set_sensitive(FALSE);
  1527.   $msg .= $d->get("unpaper missing\n");
  1528.  }
  1529.  
  1530.  # Ghost ocr item if gocr and tesseract not available
  1531.  if ( not $dependencies{gocr}
  1532.   and not $dependencies{tesseract}
  1533.   and not $dependencies{ocropus}
  1534.   and not $dependencies{cuneiform} )
  1535.  {
  1536.   $uimanager->get_widget('/MenuBar/Tools/OCR')->set_sensitive(FALSE);
  1537.   $msg .= $d->get("OCR requires gocr, tesseract, ocropus, or cuneiform\n");
  1538.  }
  1539.  
  1540.  # Put up warning if needed
  1541.  if ( $SETTING{'startup warning'} and $msg ne '' ) {
  1542.   my $dialog = Gtk2::Dialog->new( $d->get('Warning: missing packages'),
  1543.    $window, 'modal', 'gtk-ok' => 'none' );
  1544.   my $label = Gtk2::Label->new($msg);
  1545.   $dialog->vbox->add($label);
  1546.   my $cb = Gtk2::CheckButton->new_with_label(
  1547.    $d->get("Don't show this message again") );
  1548.   $cb->set_active(TRUE);
  1549.   $dialog->vbox->add($cb);
  1550.   $dialog->show_all;
  1551.   $dialog->run;
  1552.   $SETTING{'startup warning'} = FALSE if ( $cb->get_active );
  1553.   $dialog->destroy;
  1554.  }
  1555.  
  1556.  # extract the toolbar
  1557.  $toolbar = $uimanager->get_widget('/ToolBar');
  1558.  
  1559.  # turn off labels
  1560.  my $settings = $toolbar->get_settings();
  1561.  $settings->set( 'gtk-toolbar-style', 'icons' );    # only icons
  1562.  
  1563.  return;
  1564. }
  1565.  
  1566. # ghost or unghost as necessary as # pages > 0 or not.
  1567.  
  1568. sub update_uimanager {
  1569.  my @widgets = (
  1570.   '/MenuBar/View/DraggerTool',
  1571.   '/MenuBar/View/SelectorTool',
  1572.   '/MenuBar/View/Zoom 100',
  1573.   '/MenuBar/View/Zoom to fit',
  1574.   '/MenuBar/View/Zoom in',
  1575.   '/MenuBar/View/Zoom out',
  1576.   '/MenuBar/View/Rotate 90',
  1577.   '/MenuBar/View/Rotate 180',
  1578.   '/MenuBar/View/Rotate 270',
  1579.   '/MenuBar/Tools/Threshold',
  1580.   '/MenuBar/Tools/Negate',
  1581.   '/MenuBar/Tools/Unsharp',
  1582.   '/MenuBar/Tools/Crop',
  1583.   '/MenuBar/Tools/unpaper',
  1584.   '/MenuBar/Tools/OCR',
  1585.   '/MenuBar/Tools/User-defined',
  1586.  
  1587.   '/ToolBar/DraggerTool',
  1588.   '/ToolBar/SelectorTool',
  1589.   '/ToolBar/Zoom 100',
  1590.   '/ToolBar/Zoom to fit',
  1591.   '/ToolBar/Zoom in',
  1592.   '/ToolBar/Zoom out',
  1593.   '/ToolBar/Rotate 90',
  1594.   '/ToolBar/Rotate 180',
  1595.   '/ToolBar/Rotate 270',
  1596.  
  1597.   '/Detail_Popup/DraggerTool',
  1598.   '/Detail_Popup/SelectorTool',
  1599.   '/Detail_Popup/Zoom 100',
  1600.   '/Detail_Popup/Zoom to fit',
  1601.   '/Detail_Popup/Zoom in',
  1602.   '/Detail_Popup/Zoom out',
  1603.   '/Detail_Popup/Rotate 90',
  1604.   '/Detail_Popup/Rotate 180',
  1605.   '/Detail_Popup/Rotate 270',
  1606.  
  1607.   '/Thumb_Popup/Rotate 90',
  1608.   '/Thumb_Popup/Rotate 180',
  1609.   '/Thumb_Popup/Rotate 270',
  1610.  );
  1611.  
  1612.  if ( $slist->get_selected_indices ) {
  1613.   foreach (@widgets) {
  1614.    $uimanager->get_widget($_)->set_sensitive(TRUE);
  1615.   }
  1616.  }
  1617.  else {
  1618.   foreach (@widgets) {
  1619.    $uimanager->get_widget($_)->set_sensitive(FALSE);
  1620.   }
  1621.  }
  1622.  if ( $#{ $slist->{data} } > -1 ) {
  1623.   if ( $dependencies{pages} == -1 ) {
  1624.    if ( $dependencies{xdg} ) {
  1625.     $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(TRUE);
  1626.     $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(TRUE);
  1627.     $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(TRUE);
  1628.    }
  1629.    if ( $dependencies{imagemagick} and $dependencies{libtiff} ) {
  1630.     $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(TRUE);
  1631.     $uimanager->get_widget('/ToolBar/Save')->set_sensitive(TRUE);
  1632.     $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(TRUE);
  1633.    }
  1634.    $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(TRUE);
  1635.    $uimanager->get_widget('/ToolBar/Print')->set_sensitive(TRUE);
  1636.    $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(TRUE);
  1637.  
  1638.    $dependencies{pages} = $#{ $slist->{data} };
  1639.   }
  1640.  }
  1641.  else {
  1642.   if ( $dependencies{pages} > -1 ) {
  1643.    if ( $dependencies{xdg} ) {
  1644.     $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(FALSE);
  1645.     $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(FALSE);
  1646.     $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(FALSE);
  1647.     $windowe->hide if defined $windowe;
  1648.    }
  1649.    if ( $dependencies{imagemagick} and $dependencies{libtiff} ) {
  1650.     $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(FALSE);
  1651.     $uimanager->get_widget('/ToolBar/Save')->set_sensitive(FALSE);
  1652.     $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(FALSE);
  1653.    }
  1654.    $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(FALSE);
  1655.    $uimanager->get_widget('/ToolBar/Print')->set_sensitive(FALSE);
  1656.    $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(FALSE);
  1657.    $windowi->hide if defined $windowi;
  1658.  
  1659.    $dependencies{pages} = $#{ $slist->{data} };
  1660.   }
  1661.  }
  1662.  
  1663.  # If the scan dialog has already been drawn, update the start page spinbutton
  1664.  update_start();
  1665.  return;
  1666. }
  1667.  
  1668. sub display_image {
  1669.  my ($page) = @_;
  1670.  
  1671.  $current_page = $page;
  1672.  
  1673.  # quotes required to prevent File::Temp object being clobbered
  1674.  my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$current_page->{filename}");
  1675.  $view->set_pixbuf($pixbuf);
  1676.  
  1677.  # Get image dimensions to constrain selector spinbuttons on crop dialog
  1678.  ( $current_page->{w}, $current_page->{h} ) =
  1679.    ( $pixbuf->get_width, $pixbuf->get_height );
  1680.  
  1681.  # Update the ranges on the crop dialog
  1682.  if ( defined($sb_selector_w) and defined($current_page) ) {
  1683.   $sb_selector_w->set_range( 0,
  1684.    $current_page->{w} - $sb_selector_x->get_value );
  1685.   $sb_selector_h->set_range( 0,
  1686.    $current_page->{h} - $sb_selector_y->get_value );
  1687.   $sb_selector_x->set_range( 0,
  1688.    $current_page->{w} - $sb_selector_w->get_value );
  1689.   $sb_selector_y->set_range( 0,
  1690.    $current_page->{h} - $sb_selector_h->get_value );
  1691.  
  1692.   $SETTING{selection}[0] = $sb_selector_x->get_value;
  1693.   $SETTING{selection}[1] = $sb_selector_y->get_value;
  1694.   $SETTING{selection}[2] = $sb_selector_w->get_value;
  1695.   $SETTING{selection}[3] = $sb_selector_h->get_value;
  1696.  
  1697.   $selector->set_selection(
  1698.    Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
  1699.  }
  1700.  
  1701.  # Convert hocr output to Goo:Canvas if defined
  1702.  create_canvas($current_page) if ( defined $current_page->{hocr} );
  1703.  return;
  1704. }
  1705.  
  1706. sub create_canvas {
  1707.  my ($page) = @_;
  1708.  $current_page = $page;
  1709.  
  1710.  # Set up the canvas
  1711.  $canvas = Goo::Canvas->new;
  1712.  my $root = $canvas->get_root_item;
  1713.  unless ( defined $page->{w} ) {
  1714.  
  1715.   # quotes required to prevent File::Temp object being clobbered
  1716.   my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$page->{filename}");
  1717.   $page->{w} = $pixbuf->get_width;
  1718.   $page->{h} = $pixbuf->get_height;
  1719.  }
  1720.  $canvas->set_bounds( 0, 0, $page->{w}, $page->{h} );
  1721.  
  1722.  # Attach the text to the canvas
  1723.  for my $box ( $page->boxes ) {
  1724.   my ( $x1, $y1, $x2, $y2, $text ) = @$box;
  1725.   boxed_text(
  1726.    text => $text,
  1727.    x1   => $x1,
  1728.    y1   => $y1,
  1729.    x2   => $x2,
  1730.    y2   => $y2,
  1731.    size => abs( $y2 - $y1 )
  1732.   );
  1733.  }
  1734.  
  1735.  # Remove the existing contents
  1736.  $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
  1737.  
  1738.  $scwin_buffer->add($canvas);
  1739.  $canvas->set_scale( $view->get_zoom );
  1740.  $canvas->show;
  1741.  return;
  1742. }
  1743.  
  1744. # Deletes all scans after warning.
  1745.  
  1746. sub new {
  1747.  
  1748.  # Check that all pages have been saved
  1749.  for ( my $i = 0 ; $i < @{ $slist->{data} } ; $i++ ) {
  1750.   if ( not $slist->{data}[$i][2]{saved} ) {
  1751.    my $response = show_message_dialog(
  1752.     $window,
  1753.     'question',
  1754.     'ok-cancel',
  1755.     $d->get(
  1756.      "Some pages have not been saved.\nDo you really want to clear all pages?")
  1757.    );
  1758.    if ( $response ne 'ok' ) {
  1759.     return FALSE;
  1760.    }
  1761.    else {
  1762.     last;
  1763.    }
  1764.   }
  1765.  }
  1766.  
  1767.  # Update undo/redo buffers
  1768.  take_snapshot();
  1769.  
  1770.  # Deselect everything to prevent error removing selected thumbs
  1771.  $slist->get_selection->unselect_all;
  1772.  
  1773.  # Depopulate the thumbnail list
  1774.  @{ $slist->{data} } = ();
  1775.  
  1776.  # Reset start page in scan dialog
  1777.  reset_start();
  1778.  
  1779.  update_uimanager();
  1780.  return;
  1781. }
  1782.  
  1783. # Create a file filter to show only supported file types in FileChooser dialog
  1784.  
  1785. sub add_filter {
  1786.  my ( $file_chooser, $name, @file_extensions ) = @_;
  1787.  my $filter = Gtk2::FileFilter->new;
  1788.  for my $extension (@file_extensions) {
  1789.   my @filter_pattern;
  1790.  
  1791.   # Create case insensitive pattern
  1792.   for my $byte ( split //, $extension ) {
  1793.    push( @filter_pattern, '[' . uc($byte) . lc($byte) . ']' );
  1794.   }
  1795.   my $new_filter_pattern = join( '', @filter_pattern );
  1796.   $filter->add_pattern( '*.' . $new_filter_pattern );
  1797.  }
  1798.  my $types;
  1799.  for (@file_extensions) {
  1800.   if ( defined $types ) {
  1801.    $types .= ", *.$_";
  1802.   }
  1803.   else {
  1804.    $types = "*.$_";
  1805.   }
  1806.  }
  1807.  $filter->set_name("$name ($types)");
  1808.  $file_chooser->add_filter($filter);
  1809.  $filter = Gtk2::FileFilter->new;
  1810.  $filter->add_pattern('*');
  1811.  $filter->set_name('All files');
  1812.  $file_chooser->add_filter($filter);
  1813.  return;
  1814. }
  1815.  
  1816. sub open_session {
  1817.  my ($filename) = @_;
  1818.  $logger->info("Restoring session in $session");
  1819.  $slist->open_session( $session, $filename );
  1820.  update_uimanager();
  1821.  return;
  1822. }
  1823.  
  1824. # Helper function to set up thread progress bar
  1825.  
  1826. sub setup_tpbar {
  1827.  my ( $thread, $process, $completed, $total, $pid ) = @_;
  1828.  if ( $total and defined($process) ) {
  1829.   $tpbar->set_text(
  1830.    sprintf(
  1831.     $d->get("Process %i of %i (%s)"), $completed + 1, $total, $process
  1832.    )
  1833.   );
  1834.   $tpbar->set_fraction( ( $completed + 0.5 ) / $total );
  1835.   $thbox->show_all;
  1836.  
  1837. # Pass the signal back to:
  1838. # 1. be able to cancel it when the process has finished
  1839. # 2. flag that the progress bar has been set up
  1840. #    and avoid the race condition where the callback is entered before the $completed and $total variables have caught up
  1841.   return $tcbutton->signal_connect(
  1842.    clicked => sub {
  1843.     $slist->cancel($pid);
  1844.     $thbox->hide;
  1845.    }
  1846.   );
  1847.  }
  1848.  return;
  1849. }
  1850.  
  1851. # Helper function to update thread progress bar
  1852.  
  1853. sub update_tpbar {
  1854.  my (%options) = @_;
  1855.  if ( $options{total} ) {
  1856.   if ( defined $options{process_name} ) {
  1857.    $options{process_name} .= " - $options{message}"
  1858.      if ( defined $options{message} );
  1859.    $tpbar->set_text(
  1860.     sprintf(
  1861.      $d->get("Process %i of %i (%s)"),
  1862.      $options{jobs_completed} + 1, $options{jobs_total},
  1863.      $options{process_name}
  1864.     )
  1865.    );
  1866.   }
  1867.   else {
  1868.    $tpbar->set_text(
  1869.     sprintf(
  1870.      $d->get("Process %i of %i"),
  1871.      $options{jobs_completed} + 1,
  1872.      $options{jobs_total}
  1873.     )
  1874.    );
  1875.   }
  1876.   if ( defined $options{progress} ) {
  1877.    $tpbar->set_fraction(
  1878.     ( $options{jobs_completed} + $options{progress} ) / $options{jobs_total} );
  1879.   }
  1880.   else {
  1881.    $tpbar->set_fraction(
  1882.     ( $options{jobs_completed} + 0.5 ) / $options{jobs_total} );
  1883.   }
  1884.   $thbox->show_all;
  1885.   return TRUE;
  1886.  }
  1887.  return;
  1888. }
  1889.  
  1890. # Throw up file selector and open selected file
  1891.  
  1892. sub open_dialog {
  1893.  
  1894.  # cd back to cwd to get filename
  1895.  chdir $SETTING{'cwd'};
  1896.  
  1897.  my $file_chooser = Gtk2::FileChooserDialog->new(
  1898.   $d->get('Open image'),
  1899.   $window, 'open',
  1900.   'gtk-cancel' => 'cancel',
  1901.   'gtk-ok'     => 'ok'
  1902.  );
  1903.  $file_chooser->set_select_multiple(TRUE);
  1904.  $file_chooser->set_default_response('ok');
  1905.  $file_chooser->set_current_folder( $SETTING{'cwd'} );
  1906.  add_filter( $file_chooser, $d->get('Image files'),
  1907.   'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps', 'gs2p' );
  1908.  
  1909.  if ( 'ok' eq $file_chooser->run ) {
  1910.  
  1911.   # cd back to tempdir to import
  1912.   chdir $session;
  1913.  
  1914.   # Update undo/redo buffers
  1915.   take_snapshot();
  1916.  
  1917.   my @filename = $file_chooser->get_filenames;
  1918.   $file_chooser->destroy;
  1919.  
  1920.   # Update cwd
  1921.   $SETTING{'cwd'} = dirname( $filename[0] );
  1922.  
  1923.   for (@filename) {
  1924.    my ( $signal, $pid );
  1925.    $pid = $slist->get_file_info(
  1926.     path            => $_,
  1927.     queued_callback => sub {
  1928.      return update_tpbar(@_);
  1929.     },
  1930.     started_callback => sub {
  1931.      my ( $thread, $process, $completed, $total ) = @_;
  1932.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  1933.      return TRUE if ( defined $signal );
  1934.     },
  1935.     finished_callback => sub {
  1936.      my ( $info, $pending ) = @_;
  1937.      if ( $info->{format} eq 'session file' ) {
  1938.       open_session( $info->{path} );
  1939.      }
  1940.      elsif ( $info->{pages} > 1 ) {
  1941.       my $windowq = Gscan2pdf::Dialog->new(
  1942.        'transient-for' => $window,
  1943.        title           => $d->get('Pages to extract'),
  1944.        border_width    => $border_width
  1945.       );
  1946.       my $vbox = $windowq->get('vbox');
  1947.       my $hbox = Gtk2::HBox->new;
  1948.       $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  1949.       my $label = Gtk2::Label->new( $d->get('First page to extract') );
  1950.       $hbox->pack_start( $label, FALSE, FALSE, 0 );
  1951.       my $spinbuttonf =
  1952.         Gtk2::SpinButton->new_with_range( 1, $info->{pages}, 1 );
  1953.       $hbox->pack_end( $spinbuttonf, FALSE, FALSE, 0 );
  1954.       $hbox = Gtk2::HBox->new;
  1955.       $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  1956.       $label = Gtk2::Label->new( $d->get('Last page to extract') );
  1957.       $hbox->pack_start( $label, FALSE, FALSE, 0 );
  1958.       my $spinbuttonl =
  1959.         Gtk2::SpinButton->new_with_range( 1, $info->{pages}, 1 );
  1960.       $spinbuttonl->set_value( $info->{pages} );
  1961.       $hbox->pack_end( $spinbuttonl, FALSE, FALSE, 0 );
  1962.  
  1963.       # HBox for buttons
  1964.       my $hboxb = Gtk2::HBox->new;
  1965.       $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  1966.  
  1967.       # OK button
  1968.       my $obutton = Gtk2::Button->new_from_stock('gtk-ok');
  1969.       $hboxb->pack_start( $obutton, TRUE, TRUE, 0 );
  1970.       $obutton->signal_connect(
  1971.        clicked => sub {
  1972.         $windowq->destroy;
  1973.         import_file( $info, $spinbuttonf->get_value, $spinbuttonl->get_value );
  1974.        }
  1975.       );
  1976.  
  1977.       # Cancel button
  1978.       my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
  1979.       $hboxb->pack_start( $cbutton, TRUE, TRUE, 0 );
  1980.       $cbutton->signal_connect(
  1981.        clicked => sub {
  1982.         $windowq->destroy;
  1983.         return;
  1984.        }
  1985.       );
  1986.       $windowq->show_all;
  1987.      }
  1988.      else {
  1989.       import_file( $info, 1, 1 );
  1990.      }
  1991.     },
  1992.     error_callback => sub {
  1993.      my ($message) = @_;
  1994.      show_message_dialog( $window, 'error', 'close', $message );
  1995.     }
  1996.    );
  1997.   }
  1998.  }
  1999.  else {
  2000.   $file_chooser->destroy;
  2001.  }
  2002.  
  2003.  # cd back to tempdir
  2004.  chdir $session;
  2005.  return;
  2006. }
  2007.  
  2008. # Wrapper for $slist->import_file
  2009.  
  2010. sub import_file {
  2011.  my ( $info, $first_page, $last_page ) = @_;
  2012.  my ( $signal, $pid );
  2013.  $pid = $slist->import_file(
  2014.   info            => $info,
  2015.   first           => $first_page,
  2016.   last            => $last_page,
  2017.   queued_callback => sub {
  2018.    return update_tpbar(@_);
  2019.   },
  2020.   started_callback => sub {
  2021.    my ( $thread, $process, $completed, $total ) = @_;
  2022.    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  2023.    return TRUE if ( defined $signal );
  2024.   },
  2025.   running_callback => sub {
  2026.    return update_tpbar(@_);
  2027.   },
  2028.   finished_callback => sub {
  2029.    my ( $new_page, $pending ) = @_;
  2030.    $thbox->hide_all unless $pending;
  2031.    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  2032.    update_uimanager();
  2033.   },
  2034.   error_callback => sub {
  2035.    my ($message) = @_;
  2036.    show_message_dialog( $window, 'error', 'close', $message );
  2037.   }
  2038.  );
  2039.  return;
  2040. }
  2041.  
  2042. # Add a frame and radio buttons to $vbox,
  2043. sub add_page_range {
  2044.  my ($vbox) = @_;
  2045.  my $frame = Gtk2::Frame->new( $d->get('Page Range') );
  2046.  $vbox->pack_start( $frame, FALSE, FALSE, 0 );
  2047.  
  2048.  my $pr = Gscan2pdf::PageRange->new;
  2049.  $pr->set_active( $SETTING{'Page range'} )
  2050.    if ( defined $SETTING{'Page range'} );
  2051.  $pr->signal_connect(
  2052.   changed => sub {
  2053.    $SETTING{'Page range'} = $pr->get_active;
  2054.   }
  2055.  );
  2056.  $frame->add($pr);
  2057.  push @prlist, $pr;
  2058.  return;
  2059. }
  2060.  
  2061. # return string of filenames depending on which radiobutton is active
  2062.  
  2063. sub get_pagelist {
  2064.  my $n;
  2065.  my $pagelist;
  2066.  if ( $SETTING{'Page range'} eq 'all' ) {
  2067.   $n        = $#{ $slist->{data} };
  2068.   $pagelist = $slist->{data}[0][2]{filename};
  2069.   my $i = 1;
  2070.   while ( $i < @{ $slist->{data} } ) {
  2071.    $pagelist = $pagelist . " " . $slist->{data}[$i][2]{filename};
  2072.    ++$i;
  2073.   }
  2074.  }
  2075.  elsif ( $SETTING{'Page range'} eq 'selected' ) {
  2076.   my @page = $slist->get_selected_indices;
  2077.   $n        = $#page;
  2078.   $pagelist = $slist->{data}[ $page[0] ][2]{filename};
  2079.   my $i = 1;
  2080.   while ( $i < @page ) {
  2081.    $pagelist = $pagelist . " " . $slist->{data}[ $page[$i] ][2]{filename};
  2082.    ++$i;
  2083.   }
  2084.  }
  2085.  
  2086.  return ( $pagelist, $n );
  2087. }
  2088.  
  2089. # Add PDF options to $vbox
  2090.  
  2091. sub add_pdf_metadata {
  2092.  my ($vbox) = @_;
  2093.  
  2094.  # Frame for metadata
  2095.  my $frame = Gtk2::Frame->new( $d->get('Metadata') );
  2096.  $vbox->pack_start( $frame, TRUE, TRUE, 0 );
  2097.  my $vboxm = Gtk2::VBox->new;
  2098.  $vboxm->set_border_width($border_width);
  2099.  $frame->add($vboxm);
  2100.  
  2101.  # Date/time
  2102.  my $hboxe = Gtk2::HBox->new;
  2103.  $vboxm->pack_start( $hboxe, TRUE, TRUE, 0 );
  2104.  my $labele = Gtk2::Label->new( $d->get('Date') );
  2105.  $hboxe->pack_start( $labele, FALSE, FALSE, 0 );
  2106.  my ( $day, $month, $year ) =
  2107.    ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
  2108.  $year  += 1900;
  2109.  $month += 1;
  2110.  
  2111.  my $button =
  2112.    Gtk2::Button->new( sprintf( "%04d-%02d-%02d", $year, $month, $day ) );
  2113.  $button->signal_connect(
  2114.   clicked => sub {
  2115.    my $window_date = Gscan2pdf::Dialog->new(
  2116.     'transient-for' => $windowi,
  2117.     title           => $d->get('Select Date'),
  2118.     border_width    => $border_width
  2119.    );
  2120.    my $vbox_date = $window_date->get('vbox');
  2121.    $window_date->set_resizable(FALSE);
  2122.  
  2123.    my $calendar = Gtk2::Calendar->new;
  2124.    $calendar->select_day($day);
  2125.    $calendar->select_month( $month - 1, $year );
  2126.    $calendar->signal_connect(
  2127.     day_selected_double_click => sub {
  2128.      ( $year, $month, $day ) = $calendar->get_date;
  2129.      $month += 1;
  2130.      $button->set_label( sprintf( "%04d-%02d-%02d", $year, $month, $day ) );
  2131.      use Time::Local;
  2132.      $SETTING{'date offset'} = int(
  2133.       ( timelocal( 0, 0, 0, $day, $month - 1, $year ) - time ) / 60 / 60 / 24 );
  2134.      $window_date->destroy;
  2135.     }
  2136.    );
  2137.    $vbox_date->pack_start( $calendar, TRUE, TRUE, 0 );
  2138.  
  2139.    my $today = Gtk2::Button->new( $d->get('Today') );
  2140.    $today->signal_connect(
  2141.     clicked => sub {
  2142.      ( $day, $month, $year ) = ( localtime() )[ 3, 4, 5 ];
  2143.      $year += 1900;
  2144.      $calendar->select_day($day);
  2145.      $calendar->select_month( $month, $year );
  2146.     }
  2147.    );
  2148.    $vbox_date->pack_start( $today, TRUE, TRUE, 0 );
  2149.  
  2150.    $window_date->show_all;
  2151.   }
  2152.  );
  2153.  $tooltips->set_tip( $button, $d->get('Year-Month-Day') );
  2154.  $hboxe->pack_end( $button, TRUE, TRUE, 0 );
  2155.  
  2156.  # Document author
  2157.  my $hboxa = Gtk2::HBox->new;
  2158.  $vboxm->pack_start( $hboxa, TRUE, TRUE, 0 );
  2159.  my $labela = Gtk2::Label->new( $d->get('Document author') );
  2160.  $hboxa->pack_start( $labela, FALSE, FALSE, 0 );
  2161.  my $entrya = Gtk2::Entry->new;
  2162.  $hboxa->pack_end( $entrya, TRUE, TRUE, 0 );
  2163.  $entrya->set_text( $SETTING{author} ) if ( defined( $SETTING{author} ) );
  2164.  
  2165.  # Title
  2166.  my $hboxt = Gtk2::HBox->new;
  2167.  $vboxm->pack_start( $hboxt, TRUE, TRUE, 0 );
  2168.  my $labelt = Gtk2::Label->new( $d->get('Title') );
  2169.  $hboxt->pack_start( $labelt, FALSE, FALSE, 0 );
  2170.  my $entryt = Gtk2::Entry->new;
  2171.  $hboxt->pack_end( $entryt, TRUE, TRUE, 0 );
  2172.  $entryt->set_text( $SETTING{title} ) if ( defined( $SETTING{title} ) );
  2173.  
  2174.  # Subject
  2175.  my $hboxs = Gtk2::HBox->new;
  2176.  $vboxm->pack_start( $hboxs, TRUE, TRUE, 0 );
  2177.  my $labels = Gtk2::Label->new( $d->get('Subject') );
  2178.  $hboxs->pack_start( $labels, FALSE, FALSE, 0 );
  2179.  my $entrys = Gtk2::Entry->new;
  2180.  $hboxs->pack_end( $entrys, TRUE, TRUE, 0 );
  2181.  $entrys->set_text( $SETTING{subject} ) if ( defined( $SETTING{subject} ) );
  2182.  
  2183.  # Keywords
  2184.  my $hboxk = Gtk2::HBox->new;
  2185.  $vboxm->pack_start( $hboxk, TRUE, TRUE, 0 );
  2186.  my $labelk = Gtk2::Label->new( $d->get('Keywords') );
  2187.  $hboxk->pack_start( $labelk, FALSE, FALSE, 0 );
  2188.  my $entryk = Gtk2::Entry->new;
  2189.  $hboxk->pack_end( $entryk, TRUE, TRUE, 0 );
  2190.  $entryk->set_text( $SETTING{keywords} ) if ( defined( $SETTING{keywords} ) );
  2191.  
  2192.  return ( $entrya, $entryt, $entrys, $entryk );
  2193. }
  2194.  
  2195. sub update_PDF_settings {
  2196.  my ( $entrya, $entryt, $entrys, $entryk ) = @_;
  2197.  
  2198.  # Get metadata
  2199.  $SETTING{author}   = $entrya->get_text;
  2200.  $SETTING{title}    = $entryt->get_text;
  2201.  $SETTING{subject}  = $entrys->get_text;
  2202.  $SETTING{keywords} = $entryk->get_text;
  2203.  return;
  2204. }
  2205.  
  2206. sub get_pdf_metadata {
  2207.  my %h;
  2208.  $h{Author} = $SETTING{author} if defined $SETTING{author};
  2209.  my ( $day, $month, $year ) =
  2210.    ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
  2211.  $year  += 1900;
  2212.  $month += 1;
  2213.  $h{CreationDate} =
  2214.    sprintf( "D:%4i%02i%02i000000+00'00'", $year, $month, $day );
  2215.  $h{ModDate}  = sprintf( "D:%4i%02i%02i000000+00'00'", $year, $month, $day );
  2216.  $h{Creator}  = "$prog_name v$version";
  2217.  $h{Producer} = "PDF::API2";
  2218.  $h{Title}    = $SETTING{title} if defined $SETTING{title};
  2219.  $h{Subject}  = $SETTING{subject} if defined $SETTING{subject};
  2220.  $h{Keywords} = $SETTING{keywords} if defined $SETTING{keywords};
  2221.  return \%h;
  2222. }
  2223.  
  2224. # Draw text on the canvas with a box around it
  2225.  
  2226. sub boxed_text {
  2227.  my (%options) = @_;
  2228.  my $root      = $canvas->get_root_item;
  2229.  my $g         = Goo::Canvas::Group->new($root);
  2230.  $g->translate( $options{x1}, $options{y1} );
  2231.  
  2232. # draw the rect first to make sure the text goes on top and receives any mouse clicks
  2233.  my $rect = Goo::Canvas::Rect->new(
  2234.   $g, 0, 0,
  2235.   $options{x2} - $options{x1},
  2236.   $options{y2} - $options{y1}
  2237.  );
  2238.  my $text =
  2239.    Goo::Canvas::Text->new( $g, $options{text}, 0, 0,
  2240.   $options{x2} - $options{x1},
  2241.   'nw', 'height' => $options{size}, );
  2242.  
  2243.  # clicking text box produces a dialog to edit the text
  2244.  $text->signal_connect(
  2245.   'button-press-event' => sub {
  2246.    my ( $widget, $target, $ev ) = @_;
  2247.    my $dialog = Gtk2::Dialog->new(
  2248.     $d->get('Editing text') . "...", $window,
  2249.     'modal',
  2250.     'gtk-ok'     => 'ok',
  2251.     'gtk-cancel' => 'cancel'
  2252.    );
  2253.    my $textview   = Gtk2::TextView->new;
  2254.    my $textbuffer = $textview->get_buffer;
  2255.    $textbuffer->set( text => $widget->get('text') );
  2256.    $dialog->vbox->add($textview);
  2257.    $dialog->set_default_response('ok');
  2258.    $dialog->show_all;
  2259.  
  2260.    if ( $dialog->run eq 'ok' ) {
  2261.     $widget->set( text => $textbuffer->get('text') );
  2262.     canvas2hocr();
  2263.    }
  2264.    $dialog->destroy;
  2265.    $canvas->pointer_ungrab( $widget, $ev->time );
  2266.    return TRUE;
  2267.   }
  2268.  );
  2269.  
  2270.  # $rect->signal_connect(
  2271.  #  'button-press-event' => sub {
  2272.  #   my ( $widget, $target, $ev ) = @_;
  2273.  #   print "rect button-press-event\n";
  2274.  #   #  return TRUE;
  2275.  #  }
  2276.  # );
  2277.  # $g->signal_connect(
  2278.  #  'button-press-event' => sub {
  2279.  #   my ( $widget, $target, $ev ) = @_;
  2280.  #   print "group $widget button-press-event\n";
  2281.  #   my $n = $widget->get_n_children;
  2282.  #   for ( my $i = 0 ; $i < $n ; $i++ ) {
  2283.  #    my $item = $widget->get_child($i);
  2284.  #    if ( $item->isa('Goo::Canvas::Text') ) {
  2285.  #     print "contains $item\n", $item->get('text'), "\n";
  2286.  #     last;
  2287.  #    }
  2288.  #   }
  2289.  #   #  return TRUE;
  2290.  #  }
  2291.  # );
  2292.  return;
  2293. }
  2294.  
  2295. # Convert the canvas into hocr
  2296.  
  2297. sub canvas2hocr {
  2298.  my ( $x, $y, $w, $h ) = $canvas->get_bounds;
  2299.  
  2300.  $current_page->{hocr} = <<"EOS";
  2301. <!DOCTYPE html
  2302.  PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
  2303.  http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2304. <html xmlns="http://www.w3.org/1999/xhtml">
  2305. <head>
  2306.  <meta content="ocr_line ocr_page" name="ocr-capabilities"/>
  2307.  <meta content="en" name="ocr-langs"/>
  2308.  <meta content="Latn" name="ocr-scripts"/>
  2309.  <meta content="" name="ocr-microformats"/>
  2310.  <title>OCR Output</title>
  2311. </head>
  2312. <body>
  2313.  <div class="ocr_page" title="bbox $x $y $w $h>
  2314.  <p class="ocr_par">
  2315. EOS
  2316.  
  2317.  my $root = $canvas->get_root_item;
  2318.  for my $i ( 0 .. $root->get_n_children - 1 ) {
  2319.   my $group  = $root->get_child($i);
  2320.   my $bounds = $group->get_bounds;
  2321.   my ( $x1, $y1, $x2, $y2 ) =
  2322.     ( $bounds->x1 + 1, $bounds->y1 + 1, $bounds->x2 - 1, $bounds->y2 - 1 );
  2323.  
  2324. #  my $matrix = $group->get_transform; FIXME: use this as soon as Goo::Canvas 0.07 is in generally available
  2325. #  my ( $x1, $y1 ) = ( $matrix->x0, $matrix->y0 );
  2326. #  my ( $x2, $y2, $text );
  2327.   my ($text);
  2328.   for my $i ( 0 .. $group->get_n_children - 1 ) {
  2329.    my $item = $group->get_child($i);
  2330.    if ( $item->isa('Goo::Canvas::Rect') ) {
  2331.  
  2332.   #    ( $x2, $y2 ) = ( $x1 + $item->get('width'), $y1 + $item->get('height') );
  2333.    }
  2334.    elsif ( $item->isa('Goo::Canvas::Text') ) {
  2335.     $text = $item->get('text');
  2336.    }
  2337.    $current_page->{hocr} .=
  2338.      "  <span class=\"ocr_line\" title=\"bbox $x1 $y1 $x2 $y2\">$text</span>\n"
  2339.      if ( defined($x2) and defined($text) );
  2340.   }
  2341.  }
  2342.  $current_page->{hocr} .= "</p></div></body></html>";
  2343.  return;
  2344. }
  2345.  
  2346. # Throw up file selector and save selected pages as PDF under given name.
  2347.  
  2348. sub save_PDF {
  2349.  
  2350.  # cd back to cwd to save
  2351.  chdir $SETTING{'cwd'};
  2352.  
  2353.  # Set up file selector
  2354.  my $file_chooser = Gtk2::FileChooserDialog->new(
  2355.   $d->get('PDF filename'),
  2356.   $windowi, 'save',
  2357.   'gtk-cancel' => 'cancel',
  2358.   'gtk-save'   => 'ok'
  2359.  );
  2360.  
  2361.  my ( $dday, $dmonth, $dyear ) =
  2362.    ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
  2363.  $dyear += 1900;
  2364.  $dmonth = sprintf( "%02d", $dmonth + 1 );
  2365.  $dday   = sprintf( "%02d", $dday );
  2366.  my ( $tday, $tmonth, $tyear ) = ( localtime(time) )[ 3, 4, 5 ];
  2367.  $tyear += 1900;
  2368.  $tmonth = sprintf( "%02d", $tmonth + 1 );
  2369.  $tday   = sprintf( "%02d", $tday );
  2370.  
  2371.  my $filename = $SETTING{'default filename'};
  2372.  $filename =~ s/%a/$SETTING{author}/g;
  2373.  $filename =~ s/%t/$SETTING{title}/g;
  2374.  $filename =~ s/%y/$dyear/g;
  2375.  $filename =~ s/%Y/$tyear/g;
  2376.  $filename =~ s/%m/$dmonth/g;
  2377.  $filename =~ s/%M/$tmonth/g;
  2378.  $filename =~ s/%d/$dday/g;
  2379.  $filename =~ s/%D/$tday/g;
  2380.  
  2381.  $file_chooser->set_current_name($filename);
  2382.  $file_chooser->set_default_response('ok');
  2383.  $file_chooser->set_current_folder( $SETTING{'cwd'} );
  2384.  add_filter( $file_chooser, $d->get('PDF files'), 'pdf' );
  2385.  $file_chooser->set_do_overwrite_confirmation(TRUE);
  2386.  $file_chooser->signal_connect(
  2387.   response => sub {
  2388.    my ( $dialog, $response ) = @_;
  2389.    $logger->debug("PDF save filename dialog returned $response");
  2390.    if ( $response eq 'ok' ) {
  2391.     $filename = $file_chooser->get_filename;
  2392.     if ( $filename !~ /\.pdf$/ix ) {
  2393.      $filename = $filename . '.pdf';
  2394.      if ( -f $filename ) {
  2395.  
  2396.       # File exists; get the file chooser to ask the user to confirm.
  2397.       $file_chooser->set_filename($filename);
  2398.  
  2399.       # Give the name change time to take effect.
  2400.       Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  2401.       return;
  2402.      }
  2403.     }
  2404.  
  2405.     # Check that the file can be written
  2406.     if ( not -w dirname($filename) or ( -f $filename and not -w $filename ) ) {
  2407.      show_message_dialog( $file_chooser, 'error', 'close',
  2408.       sprintf( $d->get("File %s is read-only"), $filename ) );
  2409.      return;
  2410.     }
  2411.  
  2412.     # Update cwd
  2413.     $SETTING{'cwd'} = dirname($filename);
  2414.  
  2415.     # Compile list of pages
  2416.     my @list_of_pages;
  2417.     my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  2418.     for (@pagelist) {
  2419.      push @list_of_pages, $slist->{data}[$_][2];
  2420.     }
  2421.  
  2422.     # Compile options
  2423.     my %options = (
  2424.      compression      => $SETTING{'pdf compression'},
  2425.      downsample       => $SETTING{downsample},
  2426.      'downsample dpi' => $SETTING{'downsample dpi'},
  2427.      quality          => $SETTING{quality},
  2428.      font             => $SETTING{'pdf font'},
  2429.     );
  2430.  
  2431.     # Create the PDF
  2432.     $logger->debug("Started saving $filename");
  2433.     my ( $signal, $pid );
  2434.     $pid = $slist->save_pdf(
  2435.      path            => $filename,
  2436.      list_of_pages   => \@list_of_pages,
  2437.      metadata        => get_pdf_metadata(),
  2438.      options         => \%options,
  2439.      queued_callback => sub {
  2440.       return update_tpbar(@_);
  2441.      },
  2442.      started_callback => sub {
  2443.       my ( $thread, $process, $completed, $total ) = @_;
  2444.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  2445.       return TRUE if ( defined $signal );
  2446.      },
  2447.      running_callback => sub {
  2448.       return update_tpbar(@_);
  2449.      },
  2450.      finished_callback => sub {
  2451.       my ( $new_page, $pending ) = @_;
  2452.       $thbox->hide_all unless $pending;
  2453.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  2454.       mark_pages(@pagelist);
  2455.       system("xdg-open \"$filename\" &")
  2456.         if ( defined( $SETTING{'view files toggle'} )
  2457.        and $SETTING{'view files toggle'} );
  2458.       $logger->debug("Finished saving $filename");
  2459.      },
  2460.      error_callback => sub {
  2461.       my ($message) = @_;
  2462.       show_message_dialog( $window, 'error', 'close', $message );
  2463.      }
  2464.     );
  2465.  
  2466.     $windowi->hide if defined $windowi;
  2467.    }
  2468.    $file_chooser->destroy;
  2469.   }
  2470.  );
  2471.  $file_chooser->show;
  2472.  
  2473.  # cd back to tempdir
  2474.  chdir $session;
  2475.  return;
  2476. }
  2477.  
  2478. # Set up quality spinbutton here so that it can be shown or hidden by callback
  2479.  
  2480. sub add_quality_spinbutton {
  2481.  
  2482.  my ($vbox) = @_;
  2483.  my $hbox = Gtk2::HBox->new;
  2484.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  2485.  my $label = Gtk2::Label->new( $d->get('JPEG Quality') );
  2486.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  2487.  my $spinbutton = Gtk2::SpinButton->new_with_range( 1, 100, 1 );
  2488.  $spinbutton->set_value( $SETTING{'quality'} );
  2489.  $hbox->pack_end( $spinbutton, FALSE, FALSE, 0 );
  2490.  return ( $hbox, $spinbutton );
  2491. }
  2492.  
  2493. sub add_pdf_compression {
  2494.  my ($vbox) = @_;
  2495.  
  2496.  # Downsample options
  2497.  my $hboxd = Gtk2::HBox->new;
  2498.  $vbox->pack_start( $hboxd, FALSE, FALSE, 0 );
  2499.  my $button = Gtk2::CheckButton->new( $d->get('Downsample to') );
  2500.  $button->set_active(TRUE);
  2501.  $hboxd->pack_start( $button, FALSE, FALSE, 0 );
  2502.  my $spinbutton = Gtk2::SpinButton->new_with_range( 9, 2400, 1 );
  2503.  $spinbutton->set_value( $SETTING{'downsample dpi'} );
  2504.  my $label = Gtk2::Label->new( $d->get('PPI') );
  2505.  $hboxd->pack_end( $label,      FALSE, FALSE, 0 );
  2506.  $hboxd->pack_end( $spinbutton, FALSE, FALSE, 0 );
  2507.  $button->signal_connect(
  2508.   toggled => sub {
  2509.  
  2510.    if ( $button->get_active ) {
  2511.     $spinbutton->set_sensitive(TRUE);
  2512.    }
  2513.    else {
  2514.     $spinbutton->set_sensitive(FALSE);
  2515.    }
  2516.   }
  2517.  );
  2518.  $button->set_active( $SETTING{'downsample'} );
  2519.  
  2520.  # Compression options
  2521.  my @compression = (
  2522.   [
  2523.    'auto', $d->get('Automatic'),
  2524.    $d->get('Let gscan2pdf which type of compression to use.')
  2525.   ],
  2526.   [
  2527.    'lzw', $d->get('LZW'),
  2528.    $d->get('Compress output with Lempel-Ziv & Welch encoding.')
  2529.   ],
  2530.   [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate encoding.') ],
  2531.   [
  2532.    'packbits', $d->get('Packbits'),
  2533.    $d->get('Compress output with Packbits encoding.')
  2534.   ],
  2535.  
  2536. # g3 and 4 give an error message
  2537. #  [ 'g3', $d->get('G3'), $d->get('Compress output with CCITT Group 3 encoding.') ],
  2538. #  [ 'g4', $d->get('G4'), $d->get('Compress output with CCITT Group 4 encoding.') ],
  2539.   [ 'png', $d->get('PNG'),  $d->get('Compress output with PNG encoding.') ],
  2540.   [ 'jpg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  2541.   [
  2542.    'none', $d->get('None'),
  2543.    $d->get('Use no compression algorithm on output.')
  2544.   ],
  2545.  );
  2546.  
  2547.  # Compression ComboBox
  2548.  my $hbox = Gtk2::HBox->new;
  2549.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  2550.  $label = Gtk2::Label->new( $d->get('Compression') );
  2551.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  2552.  
  2553.  # Set up quality spinbutton here so that it can be shown or hidden by callback
  2554.  my ( $hboxq, $spinbuttonq ) = add_quality_spinbutton($vbox);
  2555.  my $combob = combobox_from_array(@compression);
  2556.  $combob->signal_connect(
  2557.   changed => sub {
  2558.    if ( $compression[ $combob->get_active ][0] eq 'jpg' ) {
  2559.     $hboxq->show_all;
  2560.    }
  2561.    else {
  2562.     $hboxq->hide_all;
  2563.    }
  2564.   }
  2565.  );
  2566.  combobox_set_active( $combob, $SETTING{'pdf compression'}, @compression );
  2567.  $hbox->pack_end( $combob, FALSE, FALSE, 0 );
  2568.  
  2569.  return ( $button, $spinbutton, $combob, $hboxq, $spinbuttonq, @compression );
  2570. }
  2571.  
  2572. # Display page selector and on save a fileselector.
  2573.  
  2574. sub save_dialog {
  2575.  
  2576.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  2577.  
  2578.  if ( defined $windowi ) {
  2579.   $windowi->present;
  2580.   return;
  2581.  }
  2582.  
  2583.  $windowi = Gscan2pdf::Dialog->new(
  2584.   'transient-for'  => $window,
  2585.   title            => $d->get('Save'),
  2586.   'hide-on-delete' => TRUE,
  2587.   border_width     => $border_width
  2588.  );
  2589.  my $vbox = $windowi->get('vbox');
  2590.  
  2591.  # Frame for page range
  2592.  add_page_range($vbox);
  2593.  
  2594.  # Image type ComboBox
  2595.  my $hboxi = Gtk2::HBox->new;
  2596.  $vbox->pack_start( $hboxi, TRUE, TRUE, 0 );
  2597.  my $label = Gtk2::Label->new( $d->get('Image type') );
  2598.  $hboxi->pack_start( $label, FALSE, FALSE, 0 );
  2599.  
  2600.  my @type = (
  2601.   [ 'pdf', $d->get('PDF'), $d->get('Portable Document Format') ],
  2602.   [ 'gif', $d->get('GIF'), $d->get('CompuServe graphics interchange format') ],
  2603.   [
  2604.    'jpg', $d->get('JPEG'),
  2605.    $d->get('Joint Photographic Experts Group JFIF format')
  2606.   ],
  2607.   [ 'png',     $d->get('PNG'),     $d->get('Portable Network Graphics') ],
  2608.   [ 'pnm',     $d->get('PNM'),     $d->get('Portable anymap') ],
  2609.   [ 'ps',      $d->get('PS'),      $d->get('Postscript') ],
  2610.   [ 'tif',     $d->get('TIFF'),    $d->get('Tagged Image File Format') ],
  2611.   [ 'txt',     $d->get('Text'),    $d->get('Plain text') ],
  2612.   [ 'session', $d->get('Session'), $d->get('gscan2pdf session file') ],
  2613.  );
  2614.  push @type, [ 'djvu', $d->get('DjVu'), $d->get('Deja Vu') ]
  2615.    if $dependencies{djvu};
  2616.  
  2617.  my @tiff_compression = (
  2618.   [
  2619.    'lzw', $d->get('LZW'),
  2620.    $d->get('Compress output with Lempel-Ziv & Welch encoding.')
  2621.   ],
  2622.   [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate encoding.') ],
  2623.  
  2624.   # jpeg rather than jpg needed here because tiffcp uses -c jpeg
  2625.   [ 'jpeg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  2626.   [
  2627.    'packbits', $d->get('Packbits'),
  2628.    $d->get('Compress output with Packbits encoding.')
  2629.   ],
  2630.   [
  2631.    'g3', $d->get('G3'),
  2632.    $d->get('Compress output with CCITT Group 3 encoding.')
  2633.   ],
  2634.   [
  2635.    'g4', $d->get('G4'),
  2636.    $d->get('Compress output with CCITT Group 4 encoding.')
  2637.   ],
  2638.   [
  2639.    'none', $d->get('None'),
  2640.    $d->get('Use no compression algorithm on output.')
  2641.   ],
  2642.  );
  2643.  
  2644.  # Compression ComboBox
  2645.  my $hboxc = Gtk2::HBox->new;
  2646.  $vbox->pack_start( $hboxc, FALSE, FALSE, 0 );
  2647.  $label = Gtk2::Label->new( $d->get('Compression') );
  2648.  $hboxc->pack_start( $label, FALSE, FALSE, 0 );
  2649.  
  2650.  # Set up quality spinbutton here so that it can be shown or hidden by callback
  2651.  my ( $hboxtq, $spinbuttontq ) = add_quality_spinbutton($vbox);
  2652.  $label = Gtk2::Label->new( $d->get('tiff') );
  2653.  $hboxtq->pack_start( $label, FALSE, FALSE, 0 );
  2654.  
  2655.  # Fill compression ComboBox
  2656.  my $combobtc = combobox_from_array(@tiff_compression);
  2657.  $combobtc->signal_connect(
  2658.   changed => sub {
  2659.    if ( $tiff_compression[ $combobtc->get_active ][0] eq 'jpeg' ) {
  2660.     $hboxtq->show_all;
  2661.    }
  2662.    else {
  2663.     $hboxtq->hide_all;
  2664.     $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
  2665.    }
  2666.   }
  2667.  );
  2668.  combobox_set_active( $combobtc, $SETTING{'tiff compression'},
  2669.   @tiff_compression );
  2670.  $hboxc->pack_end( $combobtc, FALSE, FALSE, 0 );
  2671.  
  2672.  # PDF options
  2673.  my $vboxp = Gtk2::VBox->new;
  2674.  $vbox->pack_start( $vboxp, FALSE, FALSE, 0 );
  2675.  my ( $entrya, $entryt, $entrys, $entryk ) = add_pdf_metadata($vboxp);
  2676.  
  2677.  # Compression options
  2678.  my ( $buttond, $spinbuttond, $combob, $hboxpq, $spinbuttonpq,
  2679.   @pdf_compression ) = add_pdf_compression($vboxp);
  2680.  
  2681.  # Font for non-ASCII text
  2682.  my $scwinf = Gtk2::ScrolledWindow->new;
  2683.  $scwinf->set_policy( 'automatic', 'automatic' );
  2684.  $vbox->pack_start( $scwinf, TRUE, TRUE, 0 );
  2685.  my $hboxf = Gtk2::HBox->new;
  2686.  $scwinf->add_with_viewport($hboxf);
  2687.  $scwinf->get_child->set_shadow_type('none');
  2688.  $label = Gtk2::Label->new( $d->get('Font for non-ASCII text') );
  2689.  $hboxf->pack_start( $label, FALSE, FALSE, 0 );
  2690.  my @fonts;
  2691.  my ($stdout) = Gscan2pdf::Document::open_three('fc-list : family style file');
  2692.  for ( split /\n/, $stdout ) {
  2693.   if (/ttf:\ /x) {
  2694.    my ( $file, $family, $style ) = split ':';
  2695.    chomp $style;
  2696.    $family =~ s/^\ //x;
  2697.    $style  =~ s/^style=//x;
  2698.    $style  =~ s/,.*$//x;
  2699.    my $font = "$family $style";
  2700.    push @fonts, [ $file, $font, $font ];
  2701.    $SETTING{'pdf font'} = $file
  2702.      if ( not defined( $SETTING{'pdf font'} )
  2703.     and $font eq 'Times New Roman Regular' );
  2704.   }
  2705.  }
  2706.  @fonts = sort { $a->[1] cmp $b->[1] } @fonts;
  2707.  my $combof = combobox_from_array(@fonts);
  2708.  combobox_set_active( $combof, $SETTING{'pdf font'}, @fonts );
  2709.  $hboxf->pack_start( $combof, FALSE, FALSE, 0 );
  2710.  
  2711.  # Fill image type ComboBox
  2712.  my $combobi = combobox_from_array(@type);
  2713.  $combobi->signal_connect(
  2714.   changed => sub {
  2715.    if ( $type[ $combobi->get_active ][0] eq 'pdf' ) {
  2716.     $vboxp->show_all;
  2717.     $hboxf->show_all;
  2718.     if ( $pdf_compression[ $combob->get_active ][0] eq 'jpg' ) {
  2719.      $hboxpq->show_all;
  2720.     }
  2721.     else {
  2722.      $hboxpq->hide_all;
  2723.      $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
  2724.     }
  2725.    }
  2726.    else {
  2727.     $vboxp->hide_all;
  2728.     $hboxf->hide_all;
  2729.     $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
  2730.    }
  2731.    if ($type[ $combobi->get_active ][0] eq 'tif'
  2732.     or $type[ $combobi->get_active ][0] eq 'ps' )
  2733.    {
  2734.     $hboxc->show_all;
  2735.    }
  2736.    else {
  2737.     $hboxc->hide_all;
  2738.     $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
  2739.    }
  2740.    if (
  2741.     $type[ $combobi->get_active ][0] eq 'jpg'
  2742.     or ( $type[ $combobi->get_active ][0] eq 'tif'
  2743.      and $tiff_compression[ $combobtc->get_active ][0] eq 'jpeg' )
  2744.      )
  2745.    {
  2746.     $hboxtq->show_all;
  2747.    }
  2748.    else {
  2749.     $hboxtq->hide_all;
  2750.     $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
  2751.    }
  2752.   }
  2753.  );
  2754.  combobox_set_active( $combobi, $SETTING{'image type'}, @type );
  2755.  $hboxi->pack_end( $combobi, FALSE, FALSE, 0 );
  2756.  
  2757.  # HBox for buttons
  2758.  my $hboxb = Gtk2::HBox->new;
  2759.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  2760.  
  2761.  # Save button
  2762.  my $sbutton = Gtk2::Button->new_from_stock('gtk-save');
  2763.  $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
  2764.  $sbutton->signal_connect(
  2765.   clicked => sub {
  2766.  
  2767.    # dig out the image type, compression and quality
  2768.    $SETTING{'image type'} = $type[ $combobi->get_active ][0];
  2769.  
  2770.    given ( $SETTING{'image type'} ) {
  2771.     when ('pdf') {
  2772.  
  2773.      # dig out the compression
  2774.      $SETTING{'downsample'}      = $buttond->get_active;
  2775.      $SETTING{'downsample dpi'}  = $spinbuttond->get_value;
  2776.      $SETTING{'pdf compression'} = $pdf_compression[ $combob->get_active ][0];
  2777.      $SETTING{'quality'}         = $spinbuttonpq->get_value;
  2778.  
  2779.      $SETTING{'pdf font'} = $fonts[ $combof->get_active ][0];
  2780.  
  2781.      update_PDF_settings( $entrya, $entryt, $entrys, $entryk );
  2782.      save_PDF();
  2783.     }
  2784.     when ('djvu') {
  2785.      save_djvu();
  2786.     }
  2787.     when ('tif') {
  2788.      $SETTING{'tiff compression'} =
  2789.        $tiff_compression[ $combobtc->get_active ][0];
  2790.      $SETTING{'quality'} = $spinbuttontq->get_value;
  2791.  
  2792.      # cd back to cwd to save
  2793.      chdir $SETTING{'cwd'};
  2794.  
  2795.      # Set up file selector
  2796.      my $file_chooser = Gtk2::FileChooserDialog->new(
  2797.       $d->get('TIFF filename'),
  2798.       $windowi, 'save',
  2799.       'gtk-cancel' => 'cancel',
  2800.       'gtk-save'   => 'ok'
  2801.      );
  2802.      $file_chooser->set_default_response('ok');
  2803.      $file_chooser->set_current_folder( $SETTING{'cwd'} );
  2804.      add_filter( $file_chooser, $d->get('Image files'),
  2805.       $SETTING{'image type'} );
  2806.      $file_chooser->set_do_overwrite_confirmation(TRUE);
  2807.      $file_chooser->signal_connect(
  2808.       response => sub {
  2809.        my ( $dialog, $response ) = @_;
  2810.        if ( $response eq 'ok' ) {
  2811.         my $filename = $file_chooser->get_filename;
  2812.         if ( $filename !~ /\.tif$/ix ) {
  2813.          $filename = $filename . '.tif';
  2814.          if ( -f $filename ) {
  2815.  
  2816.           # File exists; get the file chooser to ask the user to confirm.
  2817.           $file_chooser->set_filename($filename);
  2818.  
  2819.           # Give the name change time to take effect.
  2820.           Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  2821.           return;
  2822.          }
  2823.         }
  2824.  
  2825.         # Check that the file can be written
  2826.         if ( not -w dirname($filename)
  2827.          or ( -f $filename and not -w $filename ) )
  2828.         {
  2829.          show_message_dialog( $file_chooser, 'error', 'close',
  2830.           sprintf( $d->get("File %s is read-only"), $filename ) );
  2831.          return;
  2832.         }
  2833.  
  2834.         # Update cwd
  2835.         $SETTING{'cwd'} = dirname($filename);
  2836.  
  2837.         $windowi->hide if defined $windowi;
  2838.         save_TIFF($filename);
  2839.        }
  2840.        $file_chooser->destroy;
  2841.       }
  2842.      );
  2843.      $file_chooser->show;
  2844.  
  2845.      # cd back to tempdir
  2846.      chdir $session;
  2847.     }
  2848.     when ('txt') {
  2849.  
  2850.      # cd back to cwd to save
  2851.      chdir $SETTING{'cwd'};
  2852.  
  2853.      # Set up file selector
  2854.      my $file_chooser = Gtk2::FileChooserDialog->new(
  2855.       $d->get('Text filename'),
  2856.       $windowi, 'save',
  2857.       'gtk-cancel' => 'cancel',
  2858.       'gtk-save'   => 'ok'
  2859.      );
  2860.      $file_chooser->set_default_response('ok');
  2861.      $file_chooser->set_current_folder( $SETTING{'cwd'} );
  2862.      $file_chooser->set_do_overwrite_confirmation(TRUE);
  2863.      add_filter( $file_chooser, $d->get('Text files'), 'txt' );
  2864.      $file_chooser->signal_connect(
  2865.       response => sub {
  2866.        my ( $dialog, $response ) = @_;
  2867.        if ( $response eq 'ok' ) {
  2868.         my $filename = $file_chooser->get_filename;
  2869.         if ( $filename !~ /\.txt$/ix ) {
  2870.          $filename = $filename . '.txt';
  2871.          if ( -f $filename ) {
  2872.  
  2873.           # File exists; get the file chooser to ask the user to confirm.
  2874.           $file_chooser->set_filename($filename);
  2875.  
  2876.           # Give the name change time to take effect.
  2877.           Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  2878.           return;
  2879.          }
  2880.         }
  2881.  
  2882.         # Check that the file can be written
  2883.         if ( not -w dirname($filename)
  2884.          or ( -f $filename and not -w $filename ) )
  2885.         {
  2886.          show_message_dialog( $file_chooser, 'error', 'close',
  2887.           sprintf( $d->get("File %s is read-only"), $filename ) );
  2888.          return;
  2889.         }
  2890.  
  2891.         # Update cwd
  2892.         $SETTING{'cwd'} = dirname($filename);
  2893.  
  2894.         # Compile list of pages
  2895.         my @list_of_pages;
  2896.         my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  2897.         for (@pagelist) {
  2898.          push @list_of_pages, $slist->{data}[$_][2];
  2899.         }
  2900.  
  2901.         my ( $signal, $pid );
  2902.         $pid = $slist->save_text(
  2903.          path            => $filename,
  2904.          list_of_pages   => \@list_of_pages,
  2905.          queued_callback => sub {
  2906.           return update_tpbar(@_);
  2907.          },
  2908.          started_callback => sub {
  2909.           my ( $thread, $process, $completed, $total ) = @_;
  2910.           $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  2911.           return TRUE if ( defined $signal );
  2912.          },
  2913.          running_callback => sub {
  2914.           return update_tpbar(@_);
  2915.          },
  2916.          finished_callback => sub {
  2917.           my ( $new_page, $pending ) = @_;
  2918.           $thbox->hide_all unless $pending;
  2919.           $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  2920.           mark_pages(@pagelist);
  2921.           system("xdg-open \"$filename\" &")
  2922.             if ( defined( $SETTING{'view files toggle'} )
  2923.            and $SETTING{'view files toggle'} );
  2924.           $logger->debug("Finished saving $filename");
  2925.          },
  2926.          error_callback => sub {
  2927.           my ($message) = @_;
  2928.           show_message_dialog( $window, 'error', 'close', $message );
  2929.          }
  2930.         );
  2931.         $windowi->hide if defined $windowi;
  2932.        }
  2933.        $file_chooser->destroy;
  2934.       }
  2935.      );
  2936.      $file_chooser->show;
  2937.  
  2938.      # cd back to tempdir
  2939.      chdir $session;
  2940.     }
  2941.     when ('ps') {
  2942.      $SETTING{'tiff compression'} =
  2943.        $tiff_compression[ $combobtc->get_active ][0];
  2944.      $SETTING{'quality'} = $spinbuttontq->get_value;
  2945.  
  2946.      # cd back to cwd to save
  2947.      chdir $SETTING{'cwd'};
  2948.  
  2949.      # Set up file selector
  2950.      my $file_chooser = Gtk2::FileChooserDialog->new(
  2951.       $d->get('PS filename'),
  2952.       $windowi, 'save',
  2953.       'gtk-cancel' => 'cancel',
  2954.       'gtk-save'   => 'ok'
  2955.      );
  2956.      $file_chooser->set_default_response('ok');
  2957.      $file_chooser->set_current_folder( $SETTING{'cwd'} );
  2958.      add_filter( $file_chooser, $d->get('Postscript files'), 'ps' );
  2959.      $file_chooser->set_do_overwrite_confirmation(TRUE);
  2960.      $file_chooser->signal_connect(
  2961.       response => sub {
  2962.        my ( $dialog, $response ) = @_;
  2963.        if ( $response eq 'ok' ) {
  2964.         my $filename = $file_chooser->get_filename;
  2965.         if ( $filename !~ /\.ps$/ix ) {
  2966.          $filename = $filename . '.ps';
  2967.          if ( -f $filename ) {
  2968.  
  2969.           # File exists; get the file chooser to ask the user to confirm.
  2970.           $file_chooser->set_filename($filename);
  2971.  
  2972.           # Give the name change time to take effect.
  2973.           Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  2974.           return;
  2975.          }
  2976.         }
  2977.  
  2978.         # Check that the file can be written
  2979.         if ( not -w dirname($filename)
  2980.          or ( -f $filename and not -w $filename ) )
  2981.         {
  2982.          show_message_dialog( $file_chooser, 'error', 'close',
  2983.           sprintf( $d->get("File %s is read-only"), $filename ) );
  2984.          return;
  2985.         }
  2986.  
  2987.         # Update cwd
  2988.         $SETTING{'cwd'} = dirname($filename);
  2989.  
  2990.         # Create the PS
  2991.         my $tif = File::Temp->new( DIR => $session, SUFFIX => '.tif' );
  2992.         $windowi->hide if defined $windowi;
  2993.         save_TIFF( $tif->filename, $filename );
  2994.        }
  2995.        $file_chooser->destroy;
  2996.       }
  2997.      );
  2998.      $file_chooser->show;
  2999.  
  3000.      # cd back to tempdir
  3001.      chdir $session;
  3002.     }
  3003.     when ('session') {
  3004.  
  3005.      # cd back to cwd to save
  3006.      chdir $SETTING{'cwd'};
  3007.  
  3008.      # Set up file selector
  3009.      my $file_chooser = Gtk2::FileChooserDialog->new(
  3010.       $d->get('gscan2pdf session filename'),
  3011.       $windowi, 'save',
  3012.       'gtk-cancel' => 'cancel',
  3013.       'gtk-save'   => 'ok'
  3014.      );
  3015.      $file_chooser->set_default_response('ok');
  3016.      $file_chooser->set_current_folder( $SETTING{'cwd'} );
  3017.      add_filter( $file_chooser, $d->get('gscan2pdf session files'), 'gs2p' );
  3018.      $file_chooser->set_do_overwrite_confirmation(TRUE);
  3019.      $file_chooser->signal_connect(
  3020.       response => sub {
  3021.        my ( $dialog, $response ) = @_;
  3022.        if ( $response eq 'ok' ) {
  3023.         my $filename = $file_chooser->get_filename;
  3024.         if ( $filename !~ /\.gs2p$/ix ) {
  3025.          $filename = $filename . '.gs2p';
  3026.          if ( -f $filename ) {
  3027.  
  3028.           # File exists; get the file chooser to ask the user to confirm.
  3029.           $file_chooser->set_filename($filename);
  3030.  
  3031.           # Give the name change time to take effect.
  3032.           Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  3033.           return;
  3034.          }
  3035.         }
  3036.  
  3037.         # Check that the file can be written
  3038.         if ( not -w dirname($filename)
  3039.          or ( -f $filename and not -w $filename ) )
  3040.         {
  3041.          show_message_dialog( $file_chooser, 'error', 'close',
  3042.           sprintf( $d->get("File %s is read-only"), $filename ) );
  3043.          return;
  3044.         }
  3045.  
  3046.         # Update cwd
  3047.         $SETTING{'cwd'} = dirname($filename);
  3048.         $slist->save_session( $session, $filename );
  3049.        }
  3050.        $file_chooser->destroy;
  3051.       }
  3052.      );
  3053.      $file_chooser->show;
  3054.  
  3055.      # cd back to tempdir
  3056.      chdir $session;
  3057.     }
  3058.     default {
  3059.      save_image();
  3060.     }
  3061.    }
  3062.   }
  3063.  );
  3064.  
  3065.  # Cancel button
  3066.  my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
  3067.  $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
  3068.  $cbutton->signal_connect( clicked => sub { $windowi->hide; } );
  3069.  
  3070.  $windowi->show_all;
  3071.  $hboxpq->hide_all if ( $pdf_compression[ $combob->get_active ][0] ne 'jpg' );
  3072.  $hboxtq->hide_all
  3073.    if (
  3074.   $type[ $combobi->get_active ][0] ne 'jpg'
  3075.   or ( $type[ $combobi->get_active ][0] eq 'tif'
  3076.    and $tiff_compression[ $combobtc->get_active ][0] ne 'jpg' )
  3077.    );
  3078.  $hboxc->hide_all
  3079.    if ( $type[ $combobi->get_active ][0] ne 'tif'
  3080.   and $type[ $combobi->get_active ][0] ne 'ps' );
  3081.  
  3082.  if ( $type[ $combobi->get_active ][0] ne 'pdf' ) {
  3083.   $vboxp->hide_all;
  3084.   $hboxf->hide_all;
  3085.  }
  3086.  $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
  3087.  return;
  3088. }
  3089.  
  3090. sub file_exists {
  3091.  my ( $file_chooser, $filename ) = @_;
  3092.  if ( not -w dirname($filename) or ( -f $filename and not -w $filename ) ) {
  3093.   show_message_dialog( $file_chooser, 'error', 'close',
  3094.    sprintf( $d->get("File %s is read-only"), $filename ) );
  3095.   return TRUE;
  3096.  }
  3097.  elsif ( -e $filename ) {
  3098.   my $response =
  3099.     show_message_dialog( $file_chooser, 'question', 'ok-cancel',
  3100.    sprintf( $d->get("File %s exists.\nReally overwrite?"), $filename ) );
  3101.   return TRUE if ( $response ne 'ok' );
  3102.  }
  3103.  return FALSE;
  3104. }
  3105.  
  3106. sub save_image {
  3107.  
  3108.  # cd back to cwd to save
  3109.  chdir $SETTING{'cwd'};
  3110.  
  3111.  # Set up file selector
  3112.  my $file_chooser = Gtk2::FileChooserDialog->new(
  3113.   $d->get('Image filename'),
  3114.   $windowi, 'save',
  3115.   'gtk-cancel' => 'cancel',
  3116.   'gtk-save'   => 'ok'
  3117.  );
  3118.  $file_chooser->set_default_response('ok');
  3119.  $file_chooser->set_current_folder( $SETTING{'cwd'} );
  3120.  add_filter( $file_chooser, $d->get('Image files'),
  3121.   'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps' );
  3122.  
  3123.  if ( 'ok' eq $file_chooser->run ) {
  3124.   my $filename = $file_chooser->get_filename;
  3125.  
  3126.   # Update cwd
  3127.   $SETTING{'cwd'} = dirname($filename);
  3128.  
  3129.   # cd back to tempdir
  3130.   chdir $session;
  3131.  
  3132.   # Compile list of pages
  3133.   my @list_of_pages;
  3134.   my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  3135.   for (@pagelist) {
  3136.    push @list_of_pages, $slist->{data}[$_][2];
  3137.   }
  3138.  
  3139.   if ( @list_of_pages > 1 ) {
  3140.    my $w = length( scalar @list_of_pages );
  3141.    for ( my $i = 1 ; $i <= @list_of_pages ; $i++ ) {
  3142.     my $current_filename = sprintf "${filename}_%0${w}d.$SETTING{'image type'}",
  3143.       $i;
  3144.     return if ( file_exists( $file_chooser, $current_filename ) );
  3145.    }
  3146.    $filename = "${filename}_%0${w}d.$SETTING{'image type'}";
  3147.   }
  3148.   else {
  3149.    $filename = $filename . ".$SETTING{'image type'}"
  3150.      if ( $filename !~ /\.$SETTING{'image type'}$/ix );
  3151.    return if ( file_exists( $file_chooser, $filename ) );
  3152.   }
  3153.  
  3154.   # Create the image
  3155.   $logger->debug("Started saving $filename");
  3156.   my ( $signal, $pid );
  3157.   $pid = $slist->save_image(
  3158.    path            => $filename,
  3159.    list_of_pages   => \@list_of_pages,
  3160.    queued_callback => sub {
  3161.     return update_tpbar(@_);
  3162.    },
  3163.    started_callback => sub {
  3164.     my ( $thread, $process, $completed, $total ) = @_;
  3165.     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  3166.     return TRUE if ( defined $signal );
  3167.    },
  3168.    running_callback => sub {
  3169.     return update_tpbar(@_);
  3170.    },
  3171.    finished_callback => sub {
  3172.     my ( $new_page, $pending ) = @_;
  3173.     $thbox->hide_all unless $pending;
  3174.     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  3175.     mark_pages(@pagelist);
  3176.     system("xdg-open \"$filename\" &")
  3177.       if ( defined( $SETTING{'view files toggle'} )
  3178.      and $SETTING{'view files toggle'} );
  3179.     $logger->debug("Finished saving $filename");
  3180.    },
  3181.    error_callback => sub {
  3182.     my ($message) = @_;
  3183.     show_message_dialog( $window, 'error', 'close', $message );
  3184.    }
  3185.   );
  3186.  
  3187.   $windowi->hide if defined $windowi;
  3188.  }
  3189.  $file_chooser->destroy;
  3190.  return;
  3191. }
  3192.  
  3193. sub save_TIFF {
  3194.  my ( $filename, $ps ) = @_;
  3195.  
  3196.  # Compile list of pages
  3197.  my @list_of_pages;
  3198.  my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  3199.  for (@pagelist) {
  3200.   push @list_of_pages, $slist->{data}[$_][2];
  3201.  }
  3202.  
  3203.  # Compile options
  3204.  my %options = (
  3205.   compression => $SETTING{'tiff compression'},
  3206.   quality     => $SETTING{quality},
  3207.  );
  3208.  
  3209.  my ( $signal, $pid );
  3210.  $pid = $slist->save_tiff(
  3211.   path            => $filename,
  3212.   list_of_pages   => \@list_of_pages,
  3213.   options         => \%options,
  3214.   ps              => $ps,
  3215.   queued_callback => sub {
  3216.    return update_tpbar(@_);
  3217.   },
  3218.   started_callback => sub {
  3219.    my ( $thread, $process, $completed, $total ) = @_;
  3220.    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  3221.    return TRUE if ( defined $signal );
  3222.   },
  3223.   running_callback => sub {
  3224.    return update_tpbar(@_);
  3225.   },
  3226.   finished_callback => sub {
  3227.    my ( $new_page, $pending ) = @_;
  3228.    $thbox->hide_all unless $pending;
  3229.    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  3230.    mark_pages(@pagelist);
  3231.    my $file = defined($ps) ? $ps : $filename;
  3232.    system("xdg-open \"$filename\" &")
  3233.      if ( defined( $SETTING{'view files toggle'} )
  3234.     and $SETTING{'view files toggle'} );
  3235.    $logger->debug("Finished saving $filename");
  3236.   },
  3237.   error_callback => sub {
  3238.    my ($message) = @_;
  3239.    show_message_dialog( $window, 'error', 'close', $message );
  3240.   }
  3241.  );
  3242.  return;
  3243. }
  3244.  
  3245. sub save_djvu {
  3246.  
  3247.  # cd back to cwd to save
  3248.  chdir $SETTING{'cwd'};
  3249.  
  3250.  # Set up file selector
  3251.  my $file_chooser = Gtk2::FileChooserDialog->new(
  3252.   $d->get('DjVu filename'),
  3253.   $windowi, 'save',
  3254.   'gtk-cancel' => 'cancel',
  3255.   'gtk-save'   => 'ok'
  3256.  );
  3257.  $file_chooser->set_default_response('ok');
  3258.  $file_chooser->set_current_folder( $SETTING{'cwd'} );
  3259.  add_filter( $file_chooser, $d->get('DjVu files'), 'djvu' );
  3260.  $file_chooser->set_do_overwrite_confirmation(TRUE);
  3261.  $file_chooser->signal_connect(
  3262.   response => sub {
  3263.    my ( $dialog, $response ) = @_;
  3264.    if ( $response eq 'ok' ) {
  3265.     my $filename = $file_chooser->get_filename;
  3266.     if ( $filename !~ /\.djvu$/ix ) {
  3267.      $filename = $filename . '.djvu';
  3268.      if ( -f $filename ) {
  3269.  
  3270.       # File exists; get the file chooser to ask the user to confirm.
  3271.       $file_chooser->set_filename($filename);
  3272.  
  3273.       # Give the name change time to take effect.
  3274.       Glib::Idle->add( sub { $file_chooser->response('ok'); } );
  3275.       return;
  3276.      }
  3277.     }
  3278.  
  3279.     # Check that the file can be written
  3280.     if ( not -w dirname($filename) or ( -f $filename and not -w $filename ) ) {
  3281.      show_message_dialog( $file_chooser, 'error', 'close',
  3282.       sprintf( $d->get("File %s is read-only"), $filename ) );
  3283.      return;
  3284.     }
  3285.  
  3286.     # Update cwd
  3287.     $SETTING{'cwd'} = dirname($filename);
  3288.  
  3289.     # Compile list of pages
  3290.     my @list_of_pages;
  3291.     my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  3292.     for (@pagelist) {
  3293.      push @list_of_pages, $slist->{data}[$_][2];
  3294.     }
  3295.  
  3296.     # cd back to tempdir
  3297.     chdir $session;
  3298.  
  3299.     # Create the DjVu
  3300.     $logger->debug("Started saving $filename");
  3301.     my ( $signal, $pid );
  3302.     $pid = $slist->save_djvu(
  3303.      path            => $filename,
  3304.      list_of_pages   => \@list_of_pages,
  3305.      queued_callback => sub {
  3306.       return update_tpbar(@_);
  3307.      },
  3308.      started_callback => sub {
  3309.       my ( $thread, $process, $completed, $total ) = @_;
  3310.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  3311.       return TRUE if ( defined $signal );
  3312.      },
  3313.      running_callback => sub {
  3314.       return update_tpbar(@_);
  3315.      },
  3316.      finished_callback => sub {
  3317.       my ( $new_page, $pending ) = @_;
  3318.       $thbox->hide_all unless $pending;
  3319.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  3320.       mark_pages(@pagelist);
  3321.       system("xdg-open \"$filename\" &")
  3322.         if ( defined( $SETTING{'view files toggle'} )
  3323.        and $SETTING{'view files toggle'} );
  3324.       $logger->debug("Finished saving $filename");
  3325.      },
  3326.      error_callback => sub {
  3327.       my ($message) = @_;
  3328.       show_message_dialog( $window, 'error', 'close', $message );
  3329.      }
  3330.     );
  3331.  
  3332.     $windowi->hide if defined $windowi;
  3333.    }
  3334.    $file_chooser->destroy;
  3335.   }
  3336.  );
  3337.  $file_chooser->show;
  3338.  
  3339.  # cd back to tempdir
  3340.  chdir $session;
  3341.  return;
  3342. }
  3343.  
  3344. # Display page selector and email.
  3345.  
  3346. sub email {
  3347.  
  3348.  if ( defined $windowe ) {
  3349.   $windowe->present;
  3350.   return;
  3351.  }
  3352.  
  3353.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  3354.  $windowe = Gscan2pdf::Dialog->new(
  3355.   'transient-for'  => $window,
  3356.   title            => $d->get('Email as PDF'),
  3357.   'hide-on-delete' => TRUE,
  3358.   border_width     => $border_width
  3359.  );
  3360.  my $vbox = $windowe->get('vbox');
  3361.  
  3362.  # PDF options
  3363.  my ( $entrya, $entryt, $entrys, $entryk ) = add_pdf_metadata($vbox);
  3364.  
  3365.  # Frame for page range
  3366.  add_page_range($vbox);
  3367.  
  3368.  # Compression options
  3369.  my ( $buttond, $spinbuttond, $combob, $hboxq, $spinbuttonq, @compression ) =
  3370.    add_pdf_compression($vbox);
  3371.  
  3372.  # HBox for buttons
  3373.  my $hboxb = Gtk2::HBox->new;
  3374.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  3375.  
  3376.  # OK button
  3377.  my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
  3378.  $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
  3379.  $sbutton->signal_connect(
  3380.   clicked => sub {
  3381.  
  3382.    # Set options
  3383.    update_PDF_settings( $entrya, $entryt, $entrys, $entryk );
  3384.  
  3385.    # Compile list of pages
  3386.    my @list_of_pages;
  3387.    my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  3388.    for (@pagelist) {
  3389.     push @list_of_pages, $slist->{data}[$_][2];
  3390.    }
  3391.  
  3392.    # dig out the compression
  3393.    $SETTING{'downsample'}      = $buttond->get_active;
  3394.    $SETTING{'downsample dpi'}  = $spinbuttond->get_value;
  3395.    $SETTING{'pdf compression'} = $compression[ $combob->get_active ][0];
  3396.    $SETTING{'quality'}         = $spinbuttonq->get_value;
  3397.  
  3398.    # Compile options
  3399.    my %options = (
  3400.     compression      => $SETTING{'pdf compression'},
  3401.     downsample       => $SETTING{downsample},
  3402.     'downsample dpi' => $SETTING{'downsample dpi'},
  3403.     quality          => $SETTING{quality},
  3404.     font             => $SETTING{'pdf font'},
  3405.    );
  3406.  
  3407.    $pdf = File::Temp->new( DIR => $session, SUFFIX => '.pdf' );
  3408.  
  3409.    # Check for thunderbird
  3410.    my ($client);
  3411.    if ( defined( $ENV{KDE_FULL_SESSION} ) and $ENV{KDE_FULL_SESSION} eq 'true' )
  3412.    {
  3413.     ($client) = Gscan2pdf::Document::open_three(
  3414. "kreadconfig --file emaildefaults --group PROFILE_Default --key EmailClient| cut -d ' ' -f 1"
  3415.     );
  3416.    }
  3417.    elsif ( defined( $ENV{GNOME_DESKTOP_SESSION_ID} )
  3418.     and $ENV{GNOME_DESKTOP_SESSION_ID} ne '' )
  3419.    {
  3420.     ($client) = Gscan2pdf::Document::open_three(
  3421. "gconftool --get /desktop/gnome/url-handlers/mailto/command | cut -d ' ' -f 1"
  3422.     );
  3423.    }
  3424.  
  3425.    my $mua_string;
  3426.    if ( $client =~ /thunderbird/ ) {
  3427.     $mua_string = "thunderbird -compose attachment=file://$pdf";
  3428.    }
  3429.    else {
  3430.     $mua_string = "xdg-email --attach $pdf 'x\@y'";
  3431.    }
  3432.  
  3433.    # Create the PDF
  3434.    my ( $signal, $pid );
  3435.    $pid = $slist->save_pdf(
  3436.     path            => $pdf->filename,
  3437.     list_of_pages   => \@list_of_pages,
  3438.     metadata        => get_pdf_metadata(),
  3439.     options         => \%options,
  3440.     queued_callback => sub {
  3441.      return update_tpbar(@_);
  3442.     },
  3443.     started_callback => sub {
  3444.      my ( $thread, $process, $completed, $total ) = @_;
  3445.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  3446.      return TRUE if ( defined $signal );
  3447.     },
  3448.     running_callback => sub {
  3449.      return update_tpbar(@_);
  3450.     },
  3451.     finished_callback => sub {
  3452.      my ( $new_page, $pending ) = @_;
  3453.      $thbox->hide_all unless $pending;
  3454.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  3455.      mark_pages(@pagelist);
  3456.      system("xdg-open \"$pdf\" &")
  3457.        if ( defined( $SETTING{'view files toggle'} )
  3458.       and $SETTING{'view files toggle'} );
  3459.      $logger->info($mua_string);
  3460.      show_message_dialog( $window, 'error', 'close',
  3461.       $d->get('Error creating email') )
  3462.        if ( system($mua_string) );
  3463.     },
  3464.     error_callback => sub {
  3465.      my ($message) = @_;
  3466.      show_message_dialog( $window, 'error', 'close', $message );
  3467.     }
  3468.    );
  3469.  
  3470.    $windowe->hide;
  3471.  
  3472.   }
  3473.  );
  3474.  
  3475.  # Cancel button
  3476.  my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
  3477.  $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
  3478.  $cbutton->signal_connect( clicked => sub { $windowe->hide; } );
  3479.  
  3480.  $windowe->show_all;
  3481.  $hboxq->hide_all if ( $compression[ $combob->get_active ][0] ne 'jpg' );
  3482.  return;
  3483. }
  3484.  
  3485. # Scan
  3486.  
  3487. sub scan_dialog {
  3488.  
  3489.  if ( defined $windows ) {
  3490.   $windows->present;
  3491.   return;
  3492.  }
  3493.  
  3494.  # scan pop-up window
  3495.  $windows = Gscan2pdf::Dialog->new(
  3496.   'transient-for'  => $window,
  3497.   title            => $d->get('Scan Document'),
  3498.   'hide-on-delete' => TRUE,
  3499.   border_width     => $border_width
  3500.  );
  3501.  my $vbox = $windows->get('vbox');
  3502.  
  3503.  # HBox for devices
  3504.  my $hboxd = Gtk2::HBox->new;
  3505.  $vbox->pack_start( $hboxd, FALSE, FALSE, 0 );
  3506.  
  3507.  # Notebook to collate options
  3508.  my $notebook = Gtk2::Notebook->new;
  3509.  $notebook->set_scrollable(TRUE);
  3510.  $vbox->pack_start( $notebook, TRUE, TRUE, 0 );
  3511.  
  3512.  # Notebook page 1
  3513.  my $vbox1 = Gtk2::VBox->new;
  3514.  $notebook->append_page( $vbox1, $d->get('Page Options') );
  3515.  
  3516.  # Frame for # pages
  3517.  my $framen = Gtk2::Frame->new( $d->get('# Pages') );
  3518.  $vbox1->pack_start( $framen, FALSE, FALSE, 0 );
  3519.  my $vboxn = Gtk2::VBox->new;
  3520.  $vboxn->set_border_width($border_width);
  3521.  $framen->add($vboxn);
  3522.  
  3523.  #the first radio button has to set the group,
  3524.  #which is undef for the first button
  3525.  # All button
  3526.  $bscanall = Gtk2::RadioButton->new( undef, $d->get('All') );
  3527.  $tooltips->set_tip( $bscanall, $d->get('Scan all pages') );
  3528.  $vboxn->pack_start( $bscanall, TRUE, TRUE, 0 );
  3529.  $bscanall->signal_connect(
  3530.   clicked => sub {
  3531.    $batch_scan->set_active(0) if ( defined $batch_scan );
  3532.   }
  3533.  );
  3534.  
  3535.  # Entry button
  3536.  my $hboxn = Gtk2::HBox->new;
  3537.  $vboxn->pack_start( $hboxn, TRUE, TRUE, 0 );
  3538.  $bscannum = Gtk2::RadioButton->new( $bscanall->get_group, "#:" );
  3539.  $tooltips->set_tip( $bscannum, $d->get('Set number of pages to scan') );
  3540.  $hboxn->pack_start( $bscannum, FALSE, FALSE, 0 );
  3541.  
  3542.  # Number of pages
  3543.  $spin_buttonn = Gtk2::SpinButton->new_with_range( 1, 999, 1 );
  3544.  $tooltips->set_tip( $spin_buttonn, $d->get('Set number of pages to scan') );
  3545.  $hboxn->pack_end( $spin_buttonn, FALSE, FALSE, 0 );
  3546.  
  3547.  # Set default
  3548.  if ( $SETTING{'pages to scan'} eq 'all' ) {
  3549.   $bscanall->set_active(TRUE);
  3550.  }
  3551.  else {
  3552.   $bscannum->set_active(TRUE);
  3553.   $spin_buttonn->set_value( $SETTING{'pages to scan'} );
  3554.  }
  3555.  
  3556.  # Toggle to switch between basic and extended modes
  3557.  my $checkx = Gtk2::CheckButton->new( $d->get('Extended page numbering') );
  3558.  $vbox1->pack_start( $checkx, FALSE, FALSE, 0 );
  3559.  
  3560.  # Frame for extended mode
  3561.  my $framex = Gtk2::Frame->new( $d->get('Page number') );
  3562.  $vbox1->pack_start( $framex, FALSE, FALSE, 0 );
  3563.  my $vboxx = Gtk2::VBox->new;
  3564.  $vboxx->set_border_width($border_width);
  3565.  $framex->add($vboxx);
  3566.  
  3567.  # SpinButton for starting page number
  3568.  my $hboxxs = Gtk2::HBox->new;
  3569.  $vboxx->pack_start( $hboxxs, FALSE, FALSE, 0 );
  3570.  my $labelxs = Gtk2::Label->new( $d->get('Start') );
  3571.  $hboxxs->pack_start( $labelxs, FALSE, FALSE, 0 );
  3572.  $start = 1;
  3573.  $spin_buttons = Gtk2::SpinButton->new_with_range( 1, 99999, 1 );
  3574.  $hboxxs->pack_end( $spin_buttons, FALSE, FALSE, 0 );
  3575.  
  3576.  # SpinButton for page number increment
  3577.  my $hboxi = Gtk2::HBox->new;
  3578.  $vboxx->pack_start( $hboxi, FALSE, FALSE, 0 );
  3579.  my $labelxi = Gtk2::Label->new( $d->get('Increment') );
  3580.  $hboxi->pack_start( $labelxi, FALSE, FALSE, 0 );
  3581.  $spin_buttoni = Gtk2::SpinButton->new_with_range( -99, 99, 1 );
  3582.  my $step = 1;
  3583.  $spin_buttoni->set_value($step);
  3584.  $hboxi->pack_end( $spin_buttoni, FALSE, FALSE, 0 );
  3585.  $spin_buttoni->signal_connect(
  3586.   'value-changed' => sub {
  3587.    $spin_buttoni->set_value( -$step ) if ( $spin_buttoni->get_value == 0 );
  3588.    $step = $spin_buttoni->get_value;
  3589.   }
  3590.  );
  3591.  
  3592.  # Check whether the start page exists
  3593.  $spin_buttons->signal_connect( 'value-changed' => \&update_start );
  3594.  
  3595.  # Setting this here to fire callback running update_start
  3596.  $spin_buttons->set_value($start);
  3597.  
  3598.  # Callback on changing number of pages
  3599.  $spin_buttonn->signal_connect(
  3600.   'value-changed' => sub {
  3601.    $bscannum->set_active(TRUE);    # Set the radiobutton active
  3602.    my $n = $spin_buttonn->get_value;
  3603.    $batch_scan->set_active(0) if ( defined($batch_scan) and $n > 1 );
  3604.  
  3605.    # Check that there is room in the list for the number of pages
  3606.    update_number();
  3607.   }
  3608.  );
  3609.  
  3610.  # Frame for standard mode
  3611.  $frames = Gtk2::Frame->new( $d->get('Source document') );
  3612.  $vbox1->pack_start( $frames, FALSE, FALSE, 0 );
  3613.  my $vboxs = Gtk2::VBox->new;
  3614.  $vboxs->set_border_width($border_width);
  3615.  $frames->add($vboxs);
  3616.  
  3617.  # Single sided button
  3618.  my $buttons = Gtk2::RadioButton->new( undef, $d->get('Single sided') );
  3619.  $tooltips->set_tip( $buttons, $d->get('Source document is single-sided') );
  3620.  $vboxs->pack_start( $buttons, TRUE, TRUE, 0 );
  3621.  $buttons->signal_connect(
  3622.   clicked => sub {
  3623.    $spin_buttoni->set_value(1);
  3624.   }
  3625.  );
  3626.  
  3627.  # Double sided button
  3628.  my $buttond =
  3629.    Gtk2::RadioButton->new( $buttons->get_group, $d->get('Double sided') );
  3630.  $tooltips->set_tip( $buttond, $d->get('Source document is double-sided') );
  3631.  $vboxs->pack_start( $buttond, FALSE, FALSE, 0 );
  3632.  
  3633.  # Facing/reverse page button
  3634.  my $hboxs = Gtk2::HBox->new;
  3635.  $vboxs->pack_start( $hboxs, TRUE, TRUE, 0 );
  3636.  my $labels = Gtk2::Label->new( $d->get('Side to scan') );
  3637.  $hboxs->pack_start( $labels, FALSE, FALSE, 0 );
  3638.  
  3639.  my $combobs = Gtk2::ComboBox->new_text;
  3640.  my @side = ( $d->get('Facing'), $d->get('Reverse') );
  3641.  foreach (@side) {
  3642.   $combobs->append_text($_);
  3643.  }
  3644.  $combobs->signal_connect(
  3645.   changed => sub {
  3646.    $buttond->set_active(TRUE);    # Set the radiobutton active
  3647.    if ( $combobs->get_active == 0 ) {
  3648.     $spin_buttoni->set_value(2);
  3649.    }
  3650.    else {
  3651.     $spin_buttoni->set_value(-2);
  3652.    }
  3653.    if ( $#{ $slist->{data} } > -1 ) {
  3654.     $spin_buttons->set_value( $slist->{data}[ $#{ $slist->{data} } ][0] + 1 );
  3655.    }
  3656.    else {
  3657.     $spin_buttons->set_value(1);
  3658.    }
  3659.   }
  3660.  );
  3661.  $tooltips->set_tip( $combobs,
  3662.   $d->get('Sets which side of a double-sided document is scanned') );
  3663.  $combobs->set_active(0);
  3664.  
  3665.  # Have to do this here because setting the facing combobox switches it
  3666.  $buttons->set_active(TRUE);
  3667.  $hboxs->pack_end( $combobs, FALSE, FALSE, 0 );
  3668.  
  3669.  # Have to put the double-sided callback here to reference page side
  3670.  $buttond->signal_connect(
  3671.   clicked => sub {
  3672.    if ( $combobs->get_active == 0 ) {
  3673.     $spin_buttoni->set_value(2);
  3674.    }
  3675.    else {
  3676.     $spin_buttoni->set_value(-2);
  3677.    }
  3678.    if ( $#{ $slist->{data} } > -1 ) {
  3679.     $spin_buttons->set_value( $slist->{data}[ $#{ $slist->{data} } ][0] + 1 );
  3680.    }
  3681.    else {
  3682.     $spin_buttons->set_value(1);
  3683.    }
  3684.   }
  3685.  );
  3686.  
  3687. # Have to put the extended pagenumber checkbox here to reference simple controls
  3688.  $checkx->signal_connect(
  3689.   toggled => sub {
  3690.    if ( $checkx->get_active ) {
  3691.     $frames->hide_all;
  3692.     $framex->show_all;
  3693.    }
  3694.    else {
  3695.     if ( $spin_buttoni->get_value == 1 ) {
  3696.      $buttons->set_active(TRUE);
  3697.     }
  3698.     elsif ( $spin_buttoni->get_value > 0 ) {
  3699.      $buttond->set_active(TRUE);
  3700.      $combobs->set_active(0);
  3701.     }
  3702.     else {
  3703.      $buttond->set_active(TRUE);
  3704.      $combobs->set_active(1);
  3705.     }
  3706.     $frames->show_all;
  3707.     $framex->hide_all;
  3708.    }
  3709.   }
  3710.  );
  3711.  
  3712.  my (
  3713.   $rotate,     $side,      $side2,      $tesslang,  $cflang,
  3714.   $rbutton,    $r2button,  $ubutton,    $obutton,   $comboboxs,
  3715.   $comboboxs2, $comboboxr, $comboboxr2, $comboboxe, $comboboxtl,
  3716.   $comboboxcl, $hboxtl,    $hboxcl
  3717.  ) = add_postprocessing_options($vbox1);
  3718.  my @rotate = @$rotate;
  3719.  @side = @$side;
  3720.  my @side2    = @$side2;
  3721.  my @tesslang = @$tesslang;
  3722.  my @cflang   = @$cflang;
  3723.  
  3724.  # Notebook page 2
  3725.  my $vbox2 = Gtk2::VBox->new;
  3726.  $notebook->append_page( $vbox2, $d->get('Scan Options') );
  3727.  
  3728.  # Frame for device-dependent options
  3729.  my $framed = Gtk2::Frame->new( $d->get('Device-dependent options') );
  3730.  $vbox2->pack_start( $framed, FALSE, FALSE, 0 );
  3731.  $vboxd = Gtk2::VBox->new;
  3732.  $vboxd->set_border_width($border_width);
  3733.  $framed->add($vboxd);
  3734.  
  3735.  # Scan profiles
  3736.  my $framesp = Gtk2::Frame->new( $d->get('Scan profiles') );
  3737.  $vbox2->pack_start( $framesp, FALSE, FALSE, 0 );
  3738.  my $vboxsp = Gtk2::VBox->new;
  3739.  $vboxsp->set_border_width($border_width);
  3740.  $framesp->add($vboxsp);
  3741.  my $combobsp = Gtk2::ComboBox->new_text;
  3742.  foreach my $profile ( keys %{ $SETTING{profile} } ) {
  3743.   $combobsp->append_text($profile);
  3744.  }
  3745.  $combobsp->signal_connect(
  3746.   changed => sub {
  3747.    my $profile = $combobsp->get_active_text;
  3748.    if ( defined $profile ) {
  3749.     $logger->debug( "Setting profile $profile: ",
  3750.      Dumper( $SETTING{profile}{$profile} ) );
  3751.     if ( ref( $SETTING{profile}{$profile} ) eq 'HASH' ) {
  3752.      while ( my ( $key, $value ) = each %{ $SETTING{profile}{$profile} } ) {
  3753.       $SETTING{$key} = $value;
  3754.      }
  3755.     }
  3756.     elsif ( ref( $SETTING{profile}{$profile} ) eq 'ARRAY' ) {
  3757.      for ( @{ $SETTING{profile}{$profile} } ) {
  3758.       my ( $name, $value ) = each %$_;
  3759.       $SETTING{$name} = $value;
  3760.      }
  3761.     }
  3762.     rescan_options( $device_list[ $combobd->get_active ]{name} )
  3763.       if ( defined $combobd );
  3764.    }
  3765.   }
  3766.  );
  3767.  if ( defined $SETTING{'default profile'} ) {
  3768.   Gscan2pdf::Dialog::Scan::Sane::set_combobox_by_text( $combobsp,
  3769.    $SETTING{'default profile'} );
  3770.  }
  3771.  elsif ( Gscan2pdf::Dialog::Scan::Sane::num_rows_combobox($combobsp) > -1 ) {
  3772.   $combobsp->set_active(0);
  3773.  }
  3774.  $combobsp->set_active(
  3775.   Gscan2pdf::Dialog::Scan::Sane::num_rows_combobox($combobsp) );
  3776.  $vboxsp->pack_start( $combobsp, FALSE, FALSE, 0 );
  3777.  my $hboxsp = Gtk2::HBox->new;
  3778.  $vboxsp->pack_end( $hboxsp, FALSE, FALSE, 0 );
  3779.  
  3780.  # Save button
  3781.  my $vbutton = Gtk2::Button->new_from_stock('gtk-save');
  3782.  $vbutton->signal_connect(
  3783.   clicked => sub {
  3784.    my $dialog = Gtk2::Dialog->new(
  3785.     $d->get('Name of scan profile'), $windows,
  3786.     'destroy-with-parent',
  3787.     'gtk-save'   => 'ok',
  3788.     'gtk-cancel' => 'cancel'
  3789.    );
  3790.    my $hbox  = Gtk2::HBox->new;
  3791.    my $label = Gtk2::Label->new( $d->get('Name of scan profile') );
  3792.    $hbox->pack_start( $label, FALSE, FALSE, 0 );
  3793.    my $entry = Gtk2::Entry->new;
  3794.    $entry->set_activates_default(TRUE);
  3795.    $hbox->pack_end( $entry, TRUE, TRUE, 0 );
  3796.    $dialog->vbox->add($hbox);
  3797.    $dialog->set_default_response('ok');
  3798.    $dialog->show_all;
  3799.  
  3800.    if ( $dialog->run eq 'ok' and $entry->get_text !~ /^\s*$/x ) {
  3801.     my $profile = $entry->get_text;
  3802.     $SETTING{'default profile'} = $profile;
  3803.     my %options = walk_options_tree();
  3804.     $combobsp->append_text($profile);
  3805.     $SETTING{profile}{$profile} = \%options;
  3806.     $combobsp->set_active(
  3807.      Gscan2pdf::Dialog::Scan::Sane::num_rows_combobox($combobsp) );
  3808.    }
  3809.    $dialog->destroy;
  3810.   }
  3811.  );
  3812.  $hboxsp->pack_start( $vbutton, TRUE, TRUE, 0 );
  3813.  
  3814.  # Delete button
  3815.  my $dbutton = Gtk2::Button->new_from_stock('gtk-delete');
  3816.  $dbutton->signal_connect(
  3817.   clicked => sub {
  3818.    my $i = $combobsp->get_active;
  3819.    if ( $i > -1 ) {
  3820.     delete $SETTING{profile}{ $combobsp->get_active_text };
  3821.     $combobsp->remove_text($i);
  3822.     my $n = Gscan2pdf::Dialog::Scan::Sane::num_rows_combobox($combobsp);
  3823.     $i = $n if ( $i > $n );
  3824.     $combobsp->set_active($i) if ( $i > -1 );
  3825.    }
  3826.   }
  3827.  );
  3828.  $hboxsp->pack_start( $dbutton, FALSE, FALSE, 0 );
  3829.  
  3830.  # HBox for buttons
  3831.  my $hboxb = Gtk2::HBox->new;
  3832.  $vbox->pack_end( $hboxb, FALSE, FALSE, 0 );
  3833.  
  3834.  # Scan button
  3835.  $scan_button = Gtk2::Button->new( $d->get('Scan') );
  3836.  $hboxb->pack_start( $scan_button, TRUE, TRUE, 0 );
  3837.  $scan_button->signal_connect(
  3838.   clicked => sub {
  3839.  
  3840.    # Update undo/redo buffers
  3841.    take_snapshot();
  3842.  
  3843.    # Get selected device
  3844.    $SETTING{device} = $device_list[ $combobd->get_active ]{name};
  3845.  
  3846.    # ignore artificial paper size option
  3847.    my %options = walk_options_tree();
  3848.    delete $options{'Paper size'} if ( defined $options{'Paper size'} );
  3849.  
  3850.    # Get selected number of pages
  3851.    my $npages;
  3852.    if ( $bscannum->get_active ) {
  3853.     $SETTING{'pages to scan'} = $spin_buttonn->get_value;
  3854.     $npages = $SETTING{'pages to scan'};
  3855.    }
  3856.    else {
  3857.     $SETTING{'pages to scan'} = 'all';
  3858.     if ( $step < 0 ) {
  3859.      $npages =
  3860.        $slist->pages_possible( $spin_buttons->get_value,
  3861.       $spin_buttoni->get_value );
  3862.     }
  3863.     else {
  3864.      $npages = 0;
  3865.     }
  3866.    }
  3867.  
  3868.    $start = $spin_buttons->get_value;
  3869.    $step  = $spin_buttoni->get_value;
  3870.  
  3871.    if ( not $checkx->get_active and $start == 1 and $step < 0 ) {
  3872.     show_message_dialog( $windows, 'error', 'cancel',
  3873.      $d->get('Must scan facing pages first') );
  3874.     return TRUE;
  3875.    }
  3876.  
  3877.    $SETTING{'rotate facing'}  = 0;
  3878.    $SETTING{'rotate reverse'} = 0;
  3879.    if ( $rbutton->get_active ) {
  3880.     if ( $side[ $comboboxs->get_active ]->[0] eq 'both' ) {
  3881.      $SETTING{'rotate facing'}  = $rotate[ $comboboxr->get_active ]->[0];
  3882.      $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
  3883.     }
  3884.     elsif ( $side[ $comboboxs->get_active ]->[0] eq 'facing' ) {
  3885.      $SETTING{'rotate facing'} = $rotate[ $comboboxr->get_active ]->[0];
  3886.     }
  3887.     else {
  3888.      $SETTING{'rotate reverse'} = $rotate[ $comboboxr->get_active ]->[0];
  3889.     }
  3890.     if ( $r2button->get_active ) {
  3891.      if ( $side2[ $comboboxs2->get_active ]->[0] eq 'facing' ) {
  3892.       $SETTING{'rotate facing'} = $rotate[ $comboboxr2->get_active ]->[0];
  3893.      }
  3894.      else {
  3895.       $SETTING{'rotate reverse'} = $rotate[ $comboboxr2->get_active ]->[0];
  3896.      }
  3897.     }
  3898.    }
  3899.    $logger->info("rotate facing $SETTING{'rotate facing'}");
  3900.    $logger->info("rotate reverse $SETTING{'rotate reverse'}");
  3901.    my $rotate_facing  = $SETTING{'rotate facing'};
  3902.    my $rotate_reverse = $SETTING{'rotate reverse'};
  3903.    $logger->info( $duplex ? "" : "non-", "duplex mode" );
  3904.    if ( not $duplex ) {
  3905.     if ( $step > 0 ) {
  3906.      $rotate_reverse = $SETTING{'rotate facing'};
  3907.     }
  3908.     else {
  3909.      $rotate_facing = $SETTING{'rotate reverse'};
  3910.     }
  3911.    }
  3912.    $logger->info("rotate_facing $rotate_facing");
  3913.    $logger->info("rotate_reverse $rotate_reverse");
  3914.  
  3915.    $SETTING{'unpaper on scan'} = $ubutton->get_active;
  3916.    $logger->info("unpaper $SETTING{'unpaper on scan'}");
  3917.    $SETTING{'OCR on scan'} = $obutton->get_active;
  3918.    $logger->info("OCR $SETTING{'OCR on scan'}");
  3919.    if ( $SETTING{'OCR on scan'} ) {
  3920.     $SETTING{'ocr engine'}   = $ocr_engine[ $comboboxe->get_active ]->[0];
  3921.     $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
  3922.       if ( $SETTING{'ocr engine'} eq 'tesseract'
  3923.      or $SETTING{'ocr engine'} eq 'ocropus' );
  3924.     $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
  3925.       if ( $SETTING{'ocr engine'} eq 'cuneiform' );
  3926.    }
  3927.    scan_pages(
  3928.     device         => $SETTING{device},
  3929.     npages         => $npages,
  3930.     offset         => $start - $step,
  3931.     step           => $step,
  3932.     rotate_facing  => $rotate_facing,
  3933.     rotate_reverse => $rotate_reverse,
  3934.     unpaper        => $SETTING{'unpaper on scan'},
  3935.     ocr            => $SETTING{'OCR on scan'},
  3936.     device_options => \%options
  3937.    );
  3938.   }
  3939.  );
  3940.  
  3941.  # Cancel button
  3942.  my $cbutton = Gtk2::Button->new_from_stock('gtk-close');
  3943.  $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
  3944.  $cbutton->signal_connect( clicked => sub { $windows->hide; } );
  3945.  
  3946.  if ($test) {
  3947.   parse_device_list( $test->{output} );
  3948.   populate_device_list($hboxd);
  3949.  }
  3950.  elsif ( not @device ) {
  3951.   get_devices($hboxd);
  3952.  }
  3953.  else {
  3954.   populate_device_list($hboxd);
  3955.  }
  3956.  
  3957.  # Show window
  3958.  $windows->show_all;
  3959.  $framex->hide_all;
  3960.  $hboxtl->hide_all
  3961.    if (
  3962.   defined($hboxtl)
  3963.   and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
  3964.    or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
  3965.    );
  3966.  $hboxcl->hide_all
  3967.    if ( defined($hboxcl)
  3968.   and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) );
  3969.  
  3970. # Has to be done after showing the window, otherwise the window doesn't get centred.
  3971.  if ($test) {
  3972.   $scan_button->grab_focus;
  3973.  }
  3974.  else {
  3975.   $scan_button->set_sensitive(FALSE);
  3976.  }
  3977.  return;
  3978. }
  3979.  
  3980. sub add_postprocessing_options {
  3981.  my ($vbox) = @_;
  3982.  
  3983.  # Frame for post-processing
  3984.  my $framep = Gtk2::Frame->new( $d->get('Post-processing') );
  3985.  $vbox->pack_start( $framep, FALSE, FALSE, 0 );
  3986.  my $vboxp = Gtk2::VBox->new;
  3987.  $vboxp->set_border_width($border_width);
  3988.  $framep->add($vboxp);
  3989.  
  3990.  # Rotate
  3991.  my $hboxr = Gtk2::HBox->new;
  3992.  $vboxp->pack_start( $hboxr, FALSE, FALSE, 0 );
  3993.  my $rbutton = Gtk2::CheckButton->new( $d->get('Rotate') );
  3994.  $tooltips->set_tip( $rbutton, $d->get('Rotate image after scanning') );
  3995.  $hboxr->pack_start( $rbutton, TRUE, TRUE, 0 );
  3996.  my @side = (
  3997.   [ 'both',    $d->get('Both sides'),   $d->get('Both sides.') ],
  3998.   [ 'facing',  $d->get('Facing side'),  $d->get('Facing side.') ],
  3999.   [ 'reverse', $d->get('Reverse side'), $d->get('Reverse side.') ],
  4000.  );
  4001.  my $comboboxs = combobox_from_array(@side);
  4002.  $tooltips->set_tip( $comboboxs, $d->get('Select side to rotate') );
  4003.  $hboxr->pack_start( $comboboxs, TRUE, TRUE, 0 );
  4004.  my @rotate = (
  4005.   [ 90,  $d->get('90'),  $d->get('Rotate image 90 degrees clockwise.') ],
  4006.   [ 180, $d->get('180'), $d->get('Rotate image 180 degrees clockwise.') ],
  4007.   [ 270, $d->get('270'), $d->get('Rotate image 90 degrees anticlockwise.') ],
  4008.  );
  4009.  my $comboboxr = combobox_from_array(@rotate);
  4010.  $tooltips->set_tip( $comboboxr, $d->get('Select direction of rotation') );
  4011.  $hboxr->pack_end( $comboboxr, TRUE, TRUE, 0 );
  4012.  
  4013.  $hboxr = Gtk2::HBox->new;
  4014.  $vboxp->pack_start( $hboxr, FALSE, FALSE, 0 );
  4015.  my $r2button = Gtk2::CheckButton->new( $d->get('Rotate') );
  4016.  $tooltips->set_tip( $r2button, $d->get('Rotate image after scanning') );
  4017.  $hboxr->pack_start( $r2button, TRUE, TRUE, 0 );
  4018.  my @side2;
  4019.  my $comboboxs2 = Gtk2::ComboBox->new_text;
  4020.  $tooltips->set_tip( $comboboxs2, $d->get('Select side to rotate') );
  4021.  $hboxr->pack_start( $comboboxs2, TRUE, TRUE, 0 );
  4022.  my $comboboxr2 = combobox_from_array(@rotate);
  4023.  $tooltips->set_tip( $comboboxr2, $d->get('Select direction of rotation') );
  4024.  $hboxr->pack_end( $comboboxr2, TRUE, TRUE, 0 );
  4025.  
  4026.  $rbutton->signal_connect(
  4027.   toggled => sub {
  4028.    if ( $rbutton->get_active ) {
  4029.     $hboxr->set_sensitive(TRUE)
  4030.       if ( $side[ $comboboxs->get_active ]->[0] ne 'both' );
  4031.    }
  4032.    else {
  4033.     $hboxr->set_sensitive(FALSE);
  4034.    }
  4035.   }
  4036.  );
  4037.  $comboboxs->signal_connect(
  4038.   changed => sub {
  4039.    if ( $side[ $comboboxs->get_active ]->[0] eq 'both' ) {
  4040.     $hboxr->set_sensitive(FALSE);
  4041.     $r2button->set_active(FALSE);
  4042.    }
  4043.    else {
  4044.     $hboxr->set_sensitive(TRUE) if ( $rbutton->get_active );
  4045.  
  4046.     # Empty combobox
  4047.     while ( $comboboxs2->get_active > -1 ) {
  4048.      $comboboxs2->remove_text(0);
  4049.      $comboboxs2->set_active(0);
  4050.     }
  4051.     @side2 = ();
  4052.     foreach (@side) {
  4053.      push @side2, $_
  4054.        unless ( $_->[0] eq 'both'
  4055.       or $_->[0] eq $side[ $comboboxs->get_active ]->[0] );
  4056.     }
  4057.     $comboboxs2->append_text( $side2[0]->[1] );
  4058.     $comboboxs2->set_active(0);
  4059.    }
  4060.   }
  4061.  );
  4062.  
  4063.  # In case it isn't set elsewhere
  4064.  combobox_set_active( $comboboxr2, 90, @rotate );
  4065.  
  4066.  if ( $SETTING{'rotate facing'} or $SETTING{'rotate reverse'} ) {
  4067.   $rbutton->set_active(TRUE);
  4068.  }
  4069.  if ( $SETTING{'rotate facing'} == $SETTING{'rotate reverse'} ) {
  4070.   combobox_set_active( $comboboxs, 'both', @side );
  4071.   combobox_set_active( $comboboxr, $SETTING{'rotate facing'}, @rotate );
  4072.  }
  4073.  elsif ( $SETTING{'rotate facing'} ) {
  4074.   combobox_set_active( $comboboxs, 'facing', @side );
  4075.   combobox_set_active( $comboboxr, $SETTING{'rotate facing'}, @rotate );
  4076.   if ( $SETTING{'rotate reverse'} ) {
  4077.    $r2button->set_active(TRUE);
  4078.    combobox_set_active( $comboboxs2, 'reverse', @side2 );
  4079.    combobox_set_active( $comboboxr2, $SETTING{'rotate reverse'}, @rotate );
  4080.   }
  4081.  }
  4082.  else {
  4083.   combobox_set_active( $comboboxs, 'reverse', @side );
  4084.   combobox_set_active( $comboboxr, $SETTING{'rotate reverse'}, @rotate );
  4085.  }
  4086.  
  4087.  # CheckButton for unpaper
  4088.  my $hboxu = Gtk2::HBox->new;
  4089.  $vboxp->pack_start( $hboxu, FALSE, FALSE, 0 );
  4090.  my $ubutton = Gtk2::CheckButton->new( $d->get('Clean up images') );
  4091.  $tooltips->set_tip( $ubutton,
  4092.   $d->get('Clean up scanned images with unpaper') );
  4093.  $hboxu->pack_start( $ubutton, TRUE, TRUE, 0 );
  4094.  if ( not $dependencies{unpaper} ) {
  4095.   $ubutton->set_sensitive(FALSE);
  4096.   $ubutton->set_active(FALSE);
  4097.  }
  4098.  elsif ( $SETTING{'unpaper on scan'} ) {
  4099.   $ubutton->set_active(TRUE);
  4100.  }
  4101.  my $button = Gtk2::Button->new( $d->get('Options') );
  4102.  $tooltips->set_tip( $button, $d->get('Set unpaper options') );
  4103.  $hboxu->pack_end( $button, TRUE, TRUE, 0 );
  4104.  $button->signal_connect(
  4105.   clicked => sub {
  4106.    my $windowuo = Gscan2pdf::Dialog->new(
  4107.     'transient-for' => $window,
  4108.     title           => $d->get('unpaper options'),
  4109.     border_width    => $border_width
  4110.    );
  4111.    my $vbox1 = $windowuo->get('vbox');
  4112.    $unpaper->add_options($vbox1);
  4113.  
  4114.    # HBox for buttons
  4115.    my $hboxb = Gtk2::HBox->new;
  4116.    $vbox1->pack_start( $hboxb, FALSE, TRUE, 0 );
  4117.  
  4118.    # OK button
  4119.    my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
  4120.    $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
  4121.    $sbutton->signal_connect(
  4122.     clicked => sub {
  4123.  
  4124.      # Update $SETTING
  4125.      $SETTING{'unpaper options'} = $unpaper->get_options;
  4126.  
  4127.      $windowuo->destroy;
  4128.     }
  4129.    );
  4130.  
  4131.    # Cancel button
  4132.    my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
  4133.    $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
  4134.    $cbutton->signal_connect( clicked => sub { $windowuo->destroy; } );
  4135.  
  4136.    $windowuo->show_all;
  4137.   }
  4138.  );
  4139.  
  4140.  # CheckButton for OCR
  4141.  my $hboxo = Gtk2::HBox->new;
  4142.  $vboxp->pack_start( $hboxo, FALSE, FALSE, 0 );
  4143.  my $obutton = Gtk2::CheckButton->new( $d->get('OCR scanned pages') );
  4144.  $tooltips->set_tip( $obutton, $d->get('OCR scanned pages') );
  4145.  if ( not $dependencies{gocr}
  4146.   and not $dependencies{tesseract}
  4147.   and not $dependencies{ocropus}
  4148.   and not $dependencies{cuneiform} )
  4149.  {
  4150.   $hboxo->set_sensitive(FALSE);
  4151.   $obutton->set_active(FALSE);
  4152.  }
  4153.  elsif ( $SETTING{'OCR on scan'} ) {
  4154.   $obutton->set_active(TRUE);
  4155.  }
  4156.  $hboxo->pack_start( $obutton, TRUE, TRUE, 0 );
  4157.  my $comboboxe = combobox_from_array(@ocr_engine);
  4158.  $tooltips->set_tip( $comboboxe, $d->get('Select OCR engine') );
  4159.  $hboxo->pack_end( $comboboxe, TRUE, TRUE, 0 );
  4160.  my ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );
  4161.  if ( $dependencies{tesseract} ) {
  4162.   ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vboxp);
  4163.   $comboboxe->signal_connect(
  4164.    changed => sub {
  4165.     if ($ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
  4166.      or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
  4167.     {
  4168.      $hboxtl->show_all;
  4169.     }
  4170.     else {
  4171.      $hboxtl->hide_all;
  4172.     }
  4173.    }
  4174.   );
  4175.   if ( $dependencies{cuneiform} ) {
  4176.    ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vboxp);
  4177.    $comboboxe->signal_connect(
  4178.     changed => sub {
  4179.      if ( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) {
  4180.       $hboxcl->show_all;
  4181.      }
  4182.      else {
  4183.       $hboxcl->hide_all;
  4184.      }
  4185.     }
  4186.    );
  4187.   }
  4188.   $hboxtl->set_sensitive(FALSE) unless ( $obutton->get_active );
  4189.   $obutton->signal_connect(
  4190.    toggled => sub {
  4191.     if ( $obutton->get_active ) {
  4192.      $hboxtl->set_sensitive(TRUE);
  4193.     }
  4194.     else {
  4195.      $hboxtl->set_sensitive(FALSE);
  4196.     }
  4197.    }
  4198.   );
  4199.  }
  4200.  combobox_set_active( $comboboxe, $SETTING{'ocr engine'}, @ocr_engine );
  4201.  return (
  4202.   \@rotate,    \@side,     \@side2,     \@tesslang, \@cflang,
  4203.   $rbutton,    $r2button,  $ubutton,    $obutton,   $comboboxs,
  4204.   $comboboxs2, $comboboxr, $comboboxr2, $comboboxe, $comboboxtl,
  4205.   $comboboxcl, $hboxtl,    $hboxcl
  4206.  );
  4207. }
  4208.  
  4209. # Run scanimage --formatted-device-list
  4210.  
  4211. sub get_devices {
  4212.  my $hboxd = shift;
  4213.  
  4214.  # Set up ProgressBar
  4215.  my $pbar = Gtk2::ProgressBar->new;
  4216.  $pbar->set_pulse_step(.1);
  4217.  $pbar->set_text( $d->get('Fetching list of devices') );
  4218.  $hboxd->pack_start( $pbar, TRUE, TRUE, 0 );
  4219.  $pbar->show;
  4220.  
  4221.  Gscan2pdf::Frontend::CLI->get_devices(
  4222.   prefix            => $SETTING{'scan prefix'},
  4223.   running_callback  => sub { $pbar->pulse },
  4224.   finished_callback => sub {
  4225.    my ($device_list) = @_;
  4226.    $pbar->destroy;
  4227.  
  4228.    if ( $#{$device_list} == -1 ) {
  4229.     $windows->destroy;
  4230.     undef $windows;
  4231.     show_message_dialog( $window, 'error', 'close',
  4232.      $d->get('No devices found') );
  4233.     return;
  4234.    }
  4235.  
  4236.    # Note any duplicate device names and delete if necessary
  4237.    my %seen;
  4238.    my $i = 0;
  4239.    while ( $i < @$device_list ) {
  4240.     $seen{ $device_list->[$i]{name} }++;
  4241.     if ( $seen{ $device_list->[$i]{name} } > 1 ) {
  4242.      splice @$device_list, $i, 1;
  4243.     }
  4244.     else {
  4245.      $i++;
  4246.     }
  4247.    }
  4248.  
  4249.    # Note any duplicate model names and add the device if necessary
  4250.    undef %seen;
  4251.    for (@$device_list) {
  4252.     $_->{model} = $_->{name} unless ( defined $_->{model} );
  4253.     $seen{ $_->{model} }++;
  4254.    }
  4255.    for (@$device_list) {
  4256.     if ( defined $_->{vendor} ) {
  4257.      $_->{label} = "$_->{vendor} $_->{model}";
  4258.     }
  4259.     else {
  4260.      $_->{label} = $_->{model};
  4261.     }
  4262.     $_->{label} .= " on $_->{name}" if ( $seen{ $_->{model} } > 1 );
  4263.    }
  4264.  
  4265.    if (@device_list) {    # Update combobox
  4266.     $i = 0;
  4267.     while ( $i < @device_list ) {
  4268.      if ( @$device_list
  4269.       and $i < @$device_list
  4270.       and $device_list[$i]{label} ne $device_list->[$i]{label} )
  4271.      {
  4272.       $combobd->insert_text( $i, $device_list->[$i]{label} );
  4273.       $combobd->remove_text($i);
  4274.      }
  4275.      $i++;
  4276.     }
  4277.     while ( $i < @$device_list ) {
  4278.      $combobd->append_text( $device_list->[ $i++ ]{label} );
  4279.     }
  4280.     @device_list = @{$device_list};
  4281.     set_device();
  4282.     $combobd->show;
  4283.     $labeld->show;
  4284.    }
  4285.    else {    # New combobox
  4286.     @device_list = @{$device_list};
  4287.     populate_device_list($hboxd);
  4288.    }
  4289.  
  4290.    # # If device not set by config and there is a default device, then set it
  4291.    # $SETTING{device} = $1
  4292.    #   if ( not defined( $SETTING{device} )
  4293.    #  and defined($output)
  4294.    #  and $output =~ /default\ device\ is\ `(.*)'/x );
  4295.  
  4296.   }
  4297.  );
  4298.  return;
  4299. }
  4300.  
  4301. # Scan
  4302.  
  4303. sub scan_dialog2 {
  4304.  
  4305.  if ( defined $windows2 ) {
  4306.   $windows2->present;
  4307.   return;
  4308.  }
  4309.  
  4310.  # If device not set by config and there is a default device, then set it
  4311.  $SETTING{device} = $ENV{'SANE_DEFAULT_DEVICE'}
  4312.    if ( not defined( $SETTING{device} )
  4313.   and defined( $ENV{'SANE_DEFAULT_DEVICE'} ) );
  4314.  
  4315.  $SETTING{'pages to scan'} = 0 if ( $SETTING{'pages to scan'} eq 'all' );
  4316.  
  4317.  # scan pop-up window
  4318.  $windows2 = Gscan2pdf::Dialog::Scan::Sane->new(
  4319.   'transient-for'     => $window,
  4320.   title               => $d->get('Scan Document'),
  4321.   border_width        => $border_width,
  4322.   logger              => $logger,
  4323.   dir                 => $session,
  4324.   'hide-on-delete'    => TRUE,
  4325.   'num-pages'         => $SETTING{'pages to scan'},
  4326.   'page-number-start' => $#{ $slist->{data} } > -1
  4327.   ? $slist->{data}[ $#{ $slist->{data} } ][0] + 1
  4328.   : 1,
  4329.  );
  4330.  
  4331. # Can't set the device when creating the window, as the list does not exist then
  4332.  $windows2->signal_connect(
  4333.   'changed-device-list' => sub {
  4334.    my ( $widget, $device_list ) = @_;
  4335.    if ( defined $device_list ) {
  4336.  
  4337.     # Only set default device if it hasn't been specified on the command line
  4338.     # and it is in the the device list
  4339.     if ( defined( $SETTING{device} ) and not @device_list ) {
  4340.      for (@$device_list) {
  4341.       if ( $SETTING{device} eq $_->{name} ) {
  4342.        $windows2->set( 'device', $SETTING{device} );
  4343.        return;
  4344.       }
  4345.      }
  4346.     }
  4347.     $windows2->set( 'device', $device_list->[0]{name} );
  4348.    }
  4349.   }
  4350.  );
  4351.  
  4352.  # Update default device
  4353.  $windows2->signal_connect(
  4354.   'changed-device' => sub {
  4355.    my ( $widget, $device ) = @_;
  4356.    $SETTING{device} = $device
  4357.      if ( defined($device) and $device ne '' );
  4358.   }
  4359.  );
  4360.  
  4361.  # Set default
  4362.  $windows2->signal_connect(
  4363.   'changed-num-pages' => sub {
  4364.    ( my $widget, $SETTING{'pages to scan'} ) = @_;
  4365.  
  4366.    # Check that there is room in the list for the number of pages
  4367.    update_number();
  4368.   }
  4369.  );
  4370.  $windows2->signal_connect(
  4371.   'changed-page-number-start' => sub {
  4372.    my ( $widget, $value ) = @_;
  4373.    $windows2->set( 'max-pages',
  4374.     $slist->pages_possible( $value, $windows2->get('page-number-increment') ) );
  4375.   }
  4376.  );
  4377.  $windows2->signal_connect(
  4378.   'changed-page-number-increment' => sub {
  4379.    my ( $widget, $step ) = @_;
  4380.    $windows2->set( 'max-pages',
  4381.     $slist->pages_possible( $windows2->get('page-number-start'), $step ) );
  4382.   }
  4383.  );
  4384.  
  4385.  $windows2->signal_connect(
  4386.   'changed-side-to-scan' => sub {
  4387.    my ( $widget, $side ) = @_;
  4388.    if ( $#{ $slist->{data} } > -1 ) {
  4389.     $windows2->set( 'page-number-start',
  4390.      $slist->{data}[ $#{ $slist->{data} } ][0] + 1 );
  4391.    }
  4392.    else {
  4393.     $windows2->set( 'page-number-start', 1 );
  4394.    }
  4395.   }
  4396.  );
  4397.  
  4398.  my $signal;
  4399.  $windows2->signal_connect(
  4400.   'started-process' => sub {
  4401.    my ( $widget, $message ) = @_;
  4402.    $spbar->set_fraction(0);
  4403.    $spbar->set_text($message);
  4404.    $shbox->show_all;
  4405.    $signal = $scbutton->signal_connect(
  4406.     clicked => sub {
  4407.      $windows2->cancel_scan;
  4408.     }
  4409.    );
  4410.   }
  4411.  );
  4412.  
  4413.  $windows2->signal_connect(
  4414.   'changed-progress' => sub {
  4415.    my ( $widget, $progress, $message ) = @_;
  4416.    if ( $progress == -1 ) {
  4417.     $spbar->pulse;
  4418.    }
  4419.    else {
  4420.     $spbar->set_fraction($progress);
  4421.    }
  4422.    $spbar->set_text($message) if ( defined $message );
  4423.   }
  4424.  );
  4425.  
  4426.  $windows2->signal_connect(
  4427.   'finished-process' => sub {
  4428.    my ( $widget, $process, $button_signal ) = @_;
  4429.    $scbutton->signal_handler_disconnect($button_signal)
  4430.      if ( defined $button_signal );
  4431.    $shbox->hide;
  4432.  
  4433.    $windows2->scan_options( $SETTING{device} )
  4434.      if ( $SETTING{'cycle sane handle'} and $process eq 'scan_pages' );
  4435.   },
  4436.   $signal
  4437.  );
  4438.  
  4439.  $windows2->signal_connect(
  4440.   'process-error' => sub {
  4441.    my ( $widget, $msg ) = @_;
  4442.    $scbutton->signal_handler_disconnect($signal) if ( defined $signal );
  4443.    $shbox->hide;
  4444.    show_message_dialog( $windows2, 'error', 'close', $d_sane->get($msg) );
  4445.   }
  4446.  );
  4447.  
  4448.  $windows2->signal_connect(
  4449.   'changed-scan-option' => sub {
  4450.    my ( $widget, $option, $value ) = @_;
  4451.  
  4452.    # Note resolution
  4453.    $SETTING{resolution} = $value
  4454.      if ( $option eq SANE_NAME_SCAN_RESOLUTION );
  4455.   }
  4456.  );
  4457.  
  4458.  # Profiles
  4459.  my @profiles = keys %{ $SETTING{profile} };
  4460.  foreach my $profile (@profiles) {
  4461.   $windows2->add_profile( $profile, $SETTING{profile}{$profile} );
  4462.  }
  4463.  $windows2->signal_connect(
  4464.   'changed-profile' => sub {
  4465.    my ( $widget, $profile ) = @_;
  4466.    $SETTING{'default profile'} = $profile;
  4467.   }
  4468.  );
  4469.  $windows2->signal_connect(
  4470.   'added-profile' => sub {
  4471.    my ( $widget, $name, $profile ) = @_;
  4472.    $SETTING{profile}{$name} = $profile;
  4473.   }
  4474.  );
  4475.  $windows2->signal_connect(
  4476.   'removed-profile' => sub {
  4477.    my ( $widget, $profile ) = @_;
  4478.    delete $SETTING{profile}{$profile};
  4479.   }
  4480.  );
  4481.  
  4482.  # Update the default profile when the scan options change
  4483.  $windows2->signal_connect(
  4484.   'changed-current-scan-options' => sub {
  4485.    my ( $widget, $option_array ) = @_;
  4486.    $SETTING{'default-scan-options'} = $option_array;
  4487.   }
  4488.  );
  4489.  
  4490.  # Can't set the paper or profile until the options have been loaded
  4491.  $windows2->signal_connect(
  4492.   'reloaded-scan-options' => sub {
  4493.    $windows2->set( 'paper-formats', $SETTING{Paper} );
  4494.    $windows2->set( 'paper',         $SETTING{'Paper size'} )
  4495.      if ( defined $SETTING{'Paper size'} );
  4496.  
  4497.    if ( defined $SETTING{'default profile'} ) {
  4498.     $windows2->set( 'profile', $SETTING{'default profile'} );
  4499.    }
  4500.    elsif ( defined $SETTING{'default-scan-options'} ) {
  4501.     $windows2->set( 'current-scan-options', $SETTING{'default-scan-options'} );
  4502.    }
  4503.    elsif (@profiles) {
  4504.     $windows2->set( 'profile', $profiles[0] );
  4505.    }
  4506.   }
  4507.  );
  4508.  $windows2->signal_connect(
  4509.   'changed-paper-formats' => sub {
  4510.    my ( $widget, $formats ) = @_;
  4511.    $SETTING{Paper} = $formats;
  4512.   }
  4513.  );
  4514.  $windows2->signal_connect(
  4515.   'changed-paper' => sub {
  4516.    my ( $widget, $paper ) = @_;
  4517.    $SETTING{'Paper size'} = $paper;
  4518.   }
  4519.  );
  4520.  
  4521.  my $vbox = $windows2->get('vbox');
  4522.  my (
  4523.   $rotate,     $side,      $side2,      $tesslang,  $cflang,
  4524.   $rbutton,    $r2button,  $ubutton,    $obutton,   $comboboxs,
  4525.   $comboboxs2, $comboboxr, $comboboxr2, $comboboxe, $comboboxtl,
  4526.   $comboboxcl, $hboxtl,    $hboxcl
  4527.  ) = add_postprocessing_options($vbox);
  4528.  my @rotate   = @$rotate;
  4529.  my @side     = @$side;
  4530.  my @side2    = @$side2;
  4531.  my @tesslang = @$tesslang;
  4532.  my @cflang   = @$cflang;
  4533.  
  4534.  # Show window
  4535.  $windows2->show_all;
  4536.  $hboxtl->hide_all
  4537.    if (
  4538.   defined($hboxtl)
  4539.   and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
  4540.    or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
  4541.    );
  4542.  $hboxcl->hide_all
  4543.    if ( defined($hboxcl)
  4544.   and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) );
  4545.  
  4546.  $windows2->signal_connect(
  4547.   'new-scan' => sub {
  4548.    my ( $widget, $n ) = @_;
  4549.  
  4550.    # Update undo/redo buffers
  4551.    take_snapshot();
  4552.  
  4553.    $SETTING{'rotate facing'}  = 0;
  4554.    $SETTING{'rotate reverse'} = 0;
  4555.    if ( $rbutton->get_active ) {
  4556.     if ( $side[ $comboboxs->get_active ]->[0] eq 'both' ) {
  4557.      $SETTING{'rotate facing'}  = $rotate[ $comboboxr->get_active ]->[0];
  4558.      $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
  4559.     }
  4560.     elsif ( $side[ $comboboxs->get_active ]->[0] eq 'facing' ) {
  4561.      $SETTING{'rotate facing'} = $rotate[ $comboboxr->get_active ]->[0];
  4562.     }
  4563.     else {
  4564.      $SETTING{'rotate reverse'} = $rotate[ $comboboxr->get_active ]->[0];
  4565.     }
  4566.     if ( $r2button->get_active ) {
  4567.      if ( $side2[ $comboboxs2->get_active ]->[0] eq 'facing' ) {
  4568.       $SETTING{'rotate facing'} = $rotate[ $comboboxr2->get_active ]->[0];
  4569.      }
  4570.      else {
  4571.       $SETTING{'rotate reverse'} = $rotate[ $comboboxr2->get_active ]->[0];
  4572.      }
  4573.     }
  4574.    }
  4575.    $logger->info("rotate facing $SETTING{'rotate facing'}");
  4576.    $logger->info("rotate reverse $SETTING{'rotate reverse'}");
  4577.  
  4578.    $SETTING{'unpaper on scan'} = $ubutton->get_active;
  4579.    $logger->info("unpaper $SETTING{'unpaper on scan'}");
  4580.    $SETTING{'OCR on scan'} = $obutton->get_active;
  4581.    $logger->info("OCR $SETTING{'OCR on scan'}");
  4582.    if ( $SETTING{'OCR on scan'} ) {
  4583.     $SETTING{'ocr engine'}   = $ocr_engine[ $comboboxe->get_active ]->[0];
  4584.     $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
  4585.       if ( $SETTING{'ocr engine'} eq 'tesseract'
  4586.      or $SETTING{'ocr engine'} eq 'ocropus' );
  4587.     $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
  4588.       if ( $SETTING{'ocr engine'} eq 'cuneiform' );
  4589.    }
  4590.  
  4591.    # If the scan can't be loaded then blow the scanning dialog away and
  4592.    # show an error
  4593.    $rotate = $n % 2 ? $SETTING{'rotate facing'} : $SETTING{'rotate reverse'};
  4594.    if ( defined $test_image ) {
  4595.     import_scan(
  4596.      filename => $test_image,
  4597.      page     => $n,
  4598.      delete   => FALSE,
  4599.      rotate   => $rotate,
  4600.      unpaper  => $SETTING{'unpaper on scan'},
  4601.      ocr      => $SETTING{'OCR on scan'}
  4602.     );
  4603.    }
  4604.    else {
  4605.     $SETTING{resolution} =
  4606.       defined( $SETTING{resolution} )
  4607.       ? $SETTING{resolution}
  4608.       : $Gscan2pdf::Document::POINTS_PER_INCH;
  4609.     $logger->info("Importing scan with resolution=$SETTING{resolution}");
  4610.     import_scan(
  4611.      filename   => File::Spec->catdir( $session, "out$n.pnm" ),
  4612.      page       => $n,
  4613.      resolution => $SETTING{resolution},
  4614.      delete     => TRUE,
  4615.      rotate     => $rotate,
  4616.      unpaper    => $SETTING{'unpaper on scan'},
  4617.      ocr        => $SETTING{'OCR on scan'}
  4618.     );
  4619.    }
  4620.   }
  4621.  );
  4622.  
  4623.  if (@device_list) {
  4624.   $windows2->set( 'device-list', \@device_list );
  4625.  }
  4626.  else {
  4627.   $windows2->get_devices;
  4628.  }
  4629.  
  4630.  return;
  4631. }
  4632.  
  4633. sub walk_options_tree {
  4634.  
  4635.  # Get device-specific options
  4636.  my %options;
  4637.  if ( defined $vbox_manual_paper ) {
  4638.   foreach my $hbox ( $vboxd->get_children, $vbox_manual_paper->get_children ) {
  4639.    my $key;
  4640.    if ( $hbox->isa('Gtk2::HBox') and $hbox->sensitive ) {
  4641.     foreach my $widget ( $hbox->get_children ) {
  4642.      if ( $widget->isa('Gtk2::Label') ) {
  4643.       $key = get_key( \%ddo, $widget->get_label );
  4644.      }
  4645.      elsif ( $widget->isa('Gtk2::ComboBox') ) {
  4646.       if ( $key eq 'Paper size' ) {    ## no critic (ProhibitDeepNests)
  4647.        $SETTING{$key} = $widget->get_active_text;
  4648.       }
  4649.       else {
  4650.        $SETTING{$key} = get_value( \%ddo, $key, $widget->get_active_text );
  4651.       }
  4652.       $options{$key} = $SETTING{$key};
  4653.      }
  4654.      elsif ( $widget->isa('Gtk2::SpinButton') ) {
  4655.       $options{$key} = $widget->get_value;
  4656.       $SETTING{$key} = $options{$key};
  4657.      }
  4658.     }
  4659.    }
  4660.   }
  4661.  }
  4662.  $logger->debug( Dumper( \%options ) );
  4663.  return %options;
  4664. }
  4665.  
  4666. sub populate_device_list {
  4667.  my ($hboxd) = @_;
  4668.  
  4669.  # device list
  4670.  $labeld = Gtk2::Label->new( $d->get('Device') );
  4671.  $hboxd->pack_start( $labeld, FALSE, FALSE, 0 );
  4672.  $combobd = Gtk2::ComboBox->new_text;
  4673.  
  4674.  # read the model names into the combobox
  4675.  foreach (@device_list) {
  4676.   $combobd->append_text( $_->{label} );
  4677.  }
  4678.  $combobd->append_text( $d->get('Rescan for devices') ) unless ($test);
  4679.  
  4680.  # flags whether already run or not
  4681.  my $run = FALSE;
  4682.  $combobd->signal_connect(
  4683.   changed => sub {
  4684.    my $index = $combobd->get_active;
  4685.    if ( $index > $#device_list ) {
  4686.     $combobd->hide;
  4687.     $labeld->hide;
  4688.     get_devices($hboxd);
  4689.    }
  4690.    else {
  4691.  
  4692.     # only delete the mode setting if switching devices, not on first run
  4693.     delete $SETTING{mode} if ($run);
  4694.     $run = TRUE;
  4695.     rescan_options( $device_list[$index]{name} );
  4696.     $vboxd->show_all;
  4697.    }
  4698.   }
  4699.  );
  4700.  $tooltips->set_tip( $combobd,
  4701.   $d->get('Sets the device to be used for the scan') );
  4702.  $hboxd->pack_end( $combobd, FALSE, FALSE, 0 );
  4703.  
  4704.  # If device in settings then set it
  4705.  set_device();
  4706.  $hboxd->show_all;
  4707.  return;
  4708. }
  4709.  
  4710. sub set_device {
  4711.  my $o;
  4712.  if ( defined $SETTING{device} ) {
  4713.   for ( my $i = 0 ; $i < @device_list ; $i++ ) {
  4714.    $o = $i if ( $SETTING{device} eq $device_list[$i]{name} );
  4715.   }
  4716.  }
  4717.  unless ( defined $o ) {
  4718.   $o = 0;
  4719.   delete $SETTING{mode};
  4720.  }
  4721.  
  4722. # Set the device dependent devices after the number of pages to scan so that
  4723. #  the source button callback can ghost the all button
  4724. # This then fires the callback, updating the options, so no need to do it further down.
  4725.  $combobd->set_active($o);
  4726.  return;
  4727. }
  4728.  
  4729. # Called either from changed-value signal of spinbutton,
  4730. # or row-changed signal of simplelist
  4731.  
  4732. sub update_start {
  4733.  return unless ( defined($spin_buttons) or defined($windows2) );
  4734.  my ( $value, $step );
  4735.  if ( defined $windows2 ) {
  4736.   $value = $windows2->get('page-number-start');
  4737.   $start = $windows2->get('page-number-start') unless ( defined $start );
  4738.   $step  = $value - $start;
  4739.   $step  = $windows2->get('page-number-increment') if ( $step == 0 );
  4740.  }
  4741.  else {
  4742.   $value = $spin_buttons->get_value;
  4743.   $step  = $value - $start;
  4744.   $step  = $spin_buttoni->get_value if ( $step == 0 );
  4745.  }
  4746.  my $exists = TRUE;
  4747.  my $i = $step > 0 ? 0 : $#{ $slist->{data} };
  4748.  $start = $value;
  4749.  while ($exists) {
  4750.   if ($i < 0
  4751.    or $i > $#{ $slist->{data} }
  4752.    or ( $slist->{data}[$i][0] > $value and $step > 0 )
  4753.    or ( $slist->{data}[$i][0] < $value and $step < 0 ) )
  4754.   {
  4755.    $exists = FALSE;
  4756.   }
  4757.   elsif ( $slist->{data}[$i][0] == $value ) {
  4758.    $value += $step;
  4759.    if ( $value < 1 ) {
  4760.     $value = 1;
  4761.     $step  = 1;
  4762.    }
  4763.   }
  4764.   else {
  4765.    $i += $step > 0 ? 1 : -1;
  4766.   }
  4767.  }
  4768.  $spin_buttons->set_value($value)
  4769.    if ( defined($spin_buttons) and $start != $value );
  4770.  $windows2->set( 'page-number-start', $value ) if ( defined $windows2 );
  4771.  $start = $value;
  4772.  
  4773.  update_number();
  4774.  return;
  4775. }
  4776.  
  4777. # Update the number of pages to scan spinbutton if necessary
  4778.  
  4779. sub update_number {
  4780.  return unless ( defined($spin_buttons) or defined($windows2) );
  4781.  if ( defined $windows2 ) {
  4782.   my $n = $slist->pages_possible(
  4783.    $windows2->get('page-number-start'),
  4784.    $windows2->get('page-number-increment')
  4785.   );
  4786.   $windows2->set( 'num-pages', $n )
  4787.     if ( $n > 0 and $n < $windows2->get('num-pages') );
  4788.  }
  4789.  else {
  4790.   my $n =
  4791.     $slist->pages_possible( $spin_buttons->get_value,
  4792.    $spin_buttoni->get_value );
  4793.   $spin_buttonn->set_value($n) if ( $n > 0 and $n < $spin_buttonn->get_value );
  4794.  }
  4795.  return;
  4796. }
  4797.  
  4798. # Carry out the scan with scanimage or scanadf and the options passed.
  4799.  
  4800. sub scan_pages {
  4801.  my (%options) = @_;
  4802.  
  4803.  # Make sure we are in temp directory
  4804.  chdir $session;
  4805.  
  4806.  if ($test) {
  4807.   my $rotate = 1 % 2 ? $options{rotate_facing} : $options{rotate_reverse};
  4808.   my $index = import_scan(
  4809.    filename   => $test_image,
  4810.    page       => 1 * $options{step} + $options{offset},
  4811.    resolution => $SETTING{resolution},
  4812.    delete     => FALSE,
  4813.    rotate     => $rotate,
  4814.    unpaper    => $options{unpaper},
  4815.    ocr        => $options{ocr}
  4816.   );
  4817.  }
  4818.  else {
  4819.   my $signal;
  4820.   my @options = hash2options( %{ $options{device_options} } );
  4821.   Gscan2pdf::Frontend::CLI->scan_pages(
  4822.    prefix           => $SETTING{'scan prefix'},
  4823.    frontend         => $SETTING{frontend},
  4824.    device           => $options{device},
  4825.    npages           => $options{npages},
  4826.    options          => \@options,
  4827.    started_callback => sub {
  4828.     $spbar->set_text( $d->get('Scanning') );
  4829.     $spbar->set_pulse_step(.1);
  4830.     $shbox->show_all;
  4831.     $signal = $scbutton->signal_connect(
  4832.      clicked => sub {
  4833.       Gscan2pdf::Frontend::CLI->cancel_scan;
  4834.      }
  4835.     );
  4836.    },
  4837.    running_callback => sub {
  4838.     my ( $fraction, $text ) = @_;
  4839.     if ( defined $fraction ) {
  4840.      if ( $fraction < 0 ) {
  4841.       $spbar->pulse;
  4842.      }
  4843.      else {
  4844.       $spbar->set_fraction($fraction);
  4845.      }
  4846.     }
  4847.     $spbar->set_text($text) if ( defined $text );
  4848.    },
  4849.    new_page_callback => sub {
  4850.     my ($path) = @_;
  4851.     my $id;
  4852.     $id = $1 if ( $path =~ /^out(\d+).pnm/x );
  4853.  
  4854.     # If the scan can't be loaded then blow the scanning dialog away and
  4855.     # show an error
  4856.     my $rotate = $id % 2 ? $options{rotate_facing} : $options{rotate_reverse};
  4857.  
  4858.     my $index = import_scan(
  4859.      filename   => $path,
  4860.      page       => $id * $options{step} + $options{offset},
  4861.      resolution => $SETTING{resolution},
  4862.      delete     => TRUE,
  4863.      rotate     => $rotate,
  4864.      unpaper    => $options{unpaper},
  4865.      ocr        => $options{ocr}
  4866.     );
  4867.    },
  4868.    error_callback => sub {
  4869.     my ($text) = @_;
  4870.     show_message_dialog( $windows, 'error', 'close', $text );
  4871.    },
  4872.    finished_callback => sub {
  4873.     $scbutton->signal_handler_disconnect($signal);
  4874.     $shbox->hide;
  4875.    },
  4876.   );
  4877.  }
  4878.  return;
  4879. }
  4880.  
  4881. # Take a hash of options and push them onto an array
  4882.  
  4883. sub hash2options {
  4884.  my %options = @_;
  4885.  
  4886.  my ( @options, $key, $value );
  4887.  
  4888.  # Make sure mode is first in case of mode-dependent options
  4889.  if ( defined( $options{mode} ) ) {
  4890.   push @options, "--mode='$options{mode}'";
  4891.   delete $options{mode};
  4892.  }
  4893.  
  4894.  while ( ( $key, $value ) = each(%options) ) {
  4895.   if ( $key =~ /^[xylt]$/x ) {
  4896.    push @options, "-$key $value";
  4897.   }
  4898.   else {
  4899.    push @options, "--$key='$value'";
  4900.   }
  4901.  }
  4902.  return @options;
  4903. }
  4904.  
  4905. sub post_process_scan {
  4906.  my ( $pages, $rotate, $unpaper_flag, $ocr ) = @_;
  4907.  
  4908.  if ($rotate) {
  4909.   rotate(
  4910.    $rotate, $pages,
  4911.    sub {    # finished callback
  4912.     my ($page) = @_;
  4913.     post_process_scan( $page, undef, $unpaper_flag, $ocr );
  4914.    }
  4915.   );
  4916.  }
  4917.  elsif ($unpaper_flag) {
  4918.   unpaper_page(
  4919.    $pages,
  4920.    $unpaper->get_cmdline,
  4921.    sub {    # finished callback
  4922.     my ($page) = @_;
  4923.     post_process_scan( $page, undef, undef, $ocr );
  4924.    }
  4925.   );
  4926.  }
  4927.  elsif ($ocr) {
  4928.   ocr_page($pages);
  4929.  }
  4930.  return;
  4931. }
  4932.  
  4933. # Take new scan and display it
  4934.  
  4935. sub import_scan {
  4936.  my %options = @_;
  4937.  
  4938.  # Interface to frontend
  4939.  open my $fh, '<', $options{filename}    ## no critic (RequireBriefOpen)
  4940.    or die "can't open $options{filename}: $!\n";
  4941.  
  4942.  # Read without blocking
  4943.  my $size = 0;
  4944.  Glib::IO->add_watch(
  4945.   fileno($fh),
  4946.   [ 'in', 'hup' ],
  4947.   sub {
  4948.    my ( $fileno, $condition ) = @_;
  4949.    if ( $condition & 'in' ) {    # bit field operation. >= would also work
  4950.     if ( $size == 0 ) {
  4951.      $size = Gscan2pdf::NetPBM::file_size_from_header( $options{filename} );
  4952.      $logger->info("Header suggests $size");
  4953.      return Glib::SOURCE_CONTINUE if ( $size == 0 );
  4954.      close($fh);
  4955.     }
  4956.     my $filesize = -s $options{filename};
  4957.     $logger->info("Expecting $size, found $filesize");
  4958.     if ( $size > $filesize ) {
  4959.      my $pad = $size - $filesize;
  4960.      open( my $fh, ">>", $options{filename} )
  4961.        or die "cannot open >> $options{filename}: $!\n";
  4962.      my $data = '';
  4963.      for ( 1 .. $pad * 8 ) {
  4964.       $data .= '1';
  4965.      }
  4966.      printf $fh pack( sprintf( "b%d", length $data ), $data );
  4967.      close $fh;
  4968.      $logger->info("Padded $pad bytes");
  4969.     }
  4970.     my $gpage = Gscan2pdf::Page->new(
  4971.      filename   => $options{filename},
  4972.      resolution => $options{resolution},
  4973.      format     => 'Portable anymap',
  4974.      delete     => $options{delete},
  4975.      dir        => $session,
  4976.      window     => $window,
  4977.     );
  4978.     my $index = $slist->add_page( $gpage, $options{page}, &update_uimanager );
  4979.     if ( $index == -1 ) {
  4980.      show_message_dialog( $windows, 'error', 'close',
  4981.       $d->get('Unable to load image') );
  4982.     }
  4983.     else {
  4984.      update_uimanager();
  4985.      post_process_scan( [$gpage], $options{rotate}, $options{unpaper},
  4986.       $options{ocr} );
  4987.     }
  4988.     return Glib::SOURCE_REMOVE;
  4989.    }
  4990.    return Glib::SOURCE_CONTINUE;
  4991.   }
  4992.  );
  4993.  
  4994.  return;
  4995. }
  4996.  
  4997. # print
  4998.  
  4999. sub print_dialog {
  5000.  chdir $SETTING{'cwd'};
  5001.  my $print_op = Gtk2::PrintOperation->new;
  5002.  
  5003.  $print_op->set_print_settings($print_settings)
  5004.    if ( defined $print_settings );
  5005.  
  5006.  $print_op->signal_connect(
  5007.   begin_print => sub {
  5008.    my ( $op, $context ) = @_;
  5009.  
  5010.    my $settings = $op->get_print_settings;
  5011.    my $pages    = $settings->get('print-pages');
  5012.    my @page_list;
  5013.    if ( $pages eq 'ranges' ) {
  5014.     my $page_set = Set::IntSpan->new;
  5015.     my $ranges   = $settings->get('page-ranges');
  5016.     for ( split ',', $ranges ) {
  5017.      $page_set->I($_);
  5018.     }
  5019.     for ( 0 .. $#{ $slist->{data} } ) {
  5020.      push @page_list, $_ if ( $page_set->member( $slist->{data}[$_][0] ) );
  5021.     }
  5022.    }
  5023.    else {
  5024.     @page_list = ( 0 .. $#{ $slist->{data} } );
  5025.    }
  5026.    $op->set_n_pages( scalar(@page_list) );
  5027.   }
  5028.  );
  5029.  
  5030.  $print_op->signal_connect(
  5031.   draw_page => sub {
  5032.    my ( $op, $context, $page_number ) = @_;
  5033.  
  5034.    my $cr = $context->get_cairo_context;
  5035.  
  5036.    # Context dimensions
  5037.    my $pwidth  = $context->get_width;
  5038.    my $pheight = $context->get_height;
  5039.  
  5040.    # Image dimensions
  5041.    my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file(
  5042.     "$slist->{data}[$page_number][2]{filename}")
  5043.      ;    # quotes required to prevent File::Temp object being clobbered
  5044.    my $iwidth  = $pixbuf->get_width;
  5045.    my $iheight = $pixbuf->get_height;
  5046.  
  5047.    # Scale context to fit image
  5048.    my $scale = $pwidth / $iwidth;
  5049.    $scale = $pheight / $iheight if ( $pheight / $iheight < $scale );
  5050.    $cr->scale( $scale, $scale );
  5051.  
  5052.    # Set source pixbuf
  5053.    Gtk2::Gdk::Cairo::Context::set_source_pixbuf( $cr, $pixbuf, 0, 0 );
  5054.  
  5055.    # Paint
  5056.    $cr->paint;
  5057.  
  5058.    return;
  5059.   }
  5060.  );
  5061.  
  5062.  my $res = $print_op->run( 'print-dialog', $window );
  5063.  
  5064.  $print_settings = $print_op->get_print_settings if ( $res eq 'apply' );
  5065.  chdir $session;
  5066.  return;
  5067. }
  5068.  
  5069. # Cut the selection
  5070.  
  5071. sub cut_selection {
  5072.  if ( $slist->has_focus ) {
  5073.   copy_selection();
  5074.   delete_pages();
  5075.  }
  5076.  
  5077.  # elsif ($textview -> has_focus) {
  5078.  #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  5079.  #  $textbuffer->cut_clipboard ($clipboard, TRUE)
  5080.  # }
  5081.  return;
  5082. }
  5083.  
  5084. # Copy the selection
  5085.  
  5086. sub copy_selection {
  5087.  if ( $slist->has_focus ) {
  5088.   undef @clipboard;
  5089.   my @pages = $slist->get_selected_indices;
  5090.   for my $page (@pages) {
  5091.    my @copy = map { [@$_] } $slist->{data}[$page];
  5092.    push @clipboard, @copy;
  5093.   }
  5094.  }
  5095.  
  5096.  # elsif ($textview -> has_focus) {
  5097.  #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  5098.  #  $textbuffer->copy_clipboard ($clipboard)
  5099.  # }
  5100.  return;
  5101. }
  5102.  
  5103. # Paste the selection
  5104.  
  5105. sub paste_selection {
  5106.  
  5107.  # We only add pages if the list of thumbnails has the focus
  5108.  if ( $window->get_focus eq $slist ) {
  5109.   my @page = $slist->get_selected_indices;
  5110.  
  5111.   # Create a new image file for each page in the clipboard
  5112.   for (@clipboard) {
  5113.    my $suffix;
  5114.    $suffix = $1 if ( $_->[2]{filename} =~ /(\.\w*)$/x );
  5115.    my $new = File::Temp->new( DIR => $session, SUFFIX => $suffix );
  5116.  
  5117.    # stringify filename to prevent copy from mangling it
  5118.    unless ( copy( "$_->[2]{filename}", $new ) ) {
  5119.     show_message_dialog( $window, 'error', 'close',
  5120.      $d->get( sprintf("Error pasting image - %s"), $! ) );
  5121.     return;
  5122.    }
  5123.    $_->[2]{filename} = $new;
  5124.   }
  5125.  
  5126.   # Block the row-changed signal whilst adding the scan (row) and sorting it.
  5127.   $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  5128.  
  5129.   # If a page is selected, then insert the pasted page immediately afterwards
  5130.   if (@page) {
  5131.    splice @{ $slist->{data} }, $page[0] + 1, 0, @clipboard;
  5132.    @page = ( $page[0] + 1 );
  5133.   }
  5134.  
  5135.   # Otherwise append the page to the end
  5136.   else {
  5137.    push @{ $slist->{data} }, @clipboard;
  5138.    @page = ( $#{ $slist->{data} } - $#clipboard );
  5139.   }
  5140.  
  5141.   $slist->renumber;
  5142.   $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  5143.  
  5144.   # Select new page, deselecting others. This fires the select callback,
  5145.   # displaying the page
  5146.   $slist->get_selection->unselect_all;
  5147.   $slist->select(@page);
  5148.  
  5149.   update_uimanager();
  5150.  }
  5151.  
  5152.  # elsif ($textview -> has_focus) {
  5153.  #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
  5154.  #  $textbuffer->paste_clipboard ($clipboard, undef, TRUE)
  5155.  # }
  5156.  return;
  5157. }
  5158.  
  5159. # Delete the selected scans
  5160.  
  5161. sub delete_pages {
  5162.  
  5163.  # Update undo/redo buffers
  5164.  take_snapshot();
  5165.  
  5166.  my @pages = $slist->get_selected_indices;
  5167.  my @page  = @pages;
  5168.  $slist->get_selection->signal_handler_block(
  5169.   $slist->{selection_changed_signal} );
  5170.  while (@pages) {
  5171.   splice @{ $slist->{data} }, $pages[0], 1;
  5172.   @pages = $slist->get_selected_indices;
  5173.  }
  5174.  $slist->get_selection->signal_handler_unblock(
  5175.   $slist->{selection_changed_signal} );
  5176.  
  5177.  # Select nearest page to last current page
  5178.  if ( @{ $slist->{data} } and @page ) {
  5179.  
  5180.   # Select just the first one
  5181.   @page = ( $page[0] );
  5182.   $page[0] = $#{ $slist->{data} } if ( $page[0] > $#{ $slist->{data} } );
  5183.   $slist->select(@page);
  5184.  }
  5185.  
  5186.  # Select nothing
  5187.  elsif ( @{ $slist->{data} } ) {
  5188.   $slist->select;
  5189.  }
  5190.  
  5191. # No pages left, and having blocked the selection_changed_signal, we've got to clear the image
  5192.  else {
  5193.   $view->set_pixbuf(undef);
  5194.   $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
  5195.  }
  5196.  
  5197.  # Reset start page in scan dialog
  5198.  reset_start();
  5199.  
  5200.  update_uimanager();
  5201.  return;
  5202. }
  5203.  
  5204. # Reset start page number after delete or new
  5205.  
  5206. sub reset_start {
  5207.  if ( defined $spin_buttons ) {
  5208.   if ( $#{ $slist->{data} } > -1 ) {
  5209.    my $start_page = $spin_buttons->get_value;
  5210.    my $step       = $spin_buttoni->get_value;
  5211.    $spin_buttons->set_value( $slist->{data}[ $#{ $slist->{data} } ][0] + $step )
  5212.      if ( $start_page > $slist->{data}[ $#{ $slist->{data} } ][0] + $step );
  5213.   }
  5214.   else {
  5215.    $spin_buttons->set_value(1);
  5216.   }
  5217.  }
  5218.  elsif ( defined $windows2 ) {
  5219.   if ( $#{ $slist->{data} } > -1 ) {
  5220.    my $start_page = $windows2->get('page-number-start');
  5221.    my $step       = $windows2->get('page-number-increment');
  5222.    $windows2->set( 'page-number-start',
  5223.     $slist->{data}[ $#{ $slist->{data} } ][0] + $step )
  5224.      if ( $start_page > $slist->{data}[ $#{ $slist->{data} } ][0] + $step );
  5225.   }
  5226.   else {
  5227.    $windows2->set( 'page-number-start', 1 );
  5228.   }
  5229.  }
  5230.  return;
  5231. }
  5232.  
  5233. # Select all scans
  5234.  
  5235. sub select_all {
  5236.  
  5237.  # if ($textview -> has_focus) {
  5238.  #  my ($start, $end) = $textbuffer->get_bounds;
  5239.  #  $textbuffer->select_range ($start, $end);
  5240.  # }
  5241.  # else {
  5242.  $slist->get_selection->select_all;
  5243.  
  5244.  # }
  5245.  return;
  5246. }
  5247.  
  5248. # Select all odd(0) or even(1) scans
  5249.  
  5250. sub select_odd_even {
  5251.  my $odd = shift;
  5252.  my @selection;
  5253.  for ( 0 .. $#{ $slist->{data} } ) {
  5254.   push @selection, $_ if ( $slist->{data}[$_][0] % 2 xor $odd );
  5255.  }
  5256.  
  5257.  $slist->get_selection->unselect_all;
  5258.  $slist->select(@selection);
  5259.  return;
  5260. }
  5261.  
  5262. sub select_modified_since_ocr {
  5263.  my @selection;
  5264.  foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  5265.   my $dirty_time = $slist->{data}[$page][2]{dirty_time};
  5266.   my $ocr_flag   = $slist->{data}[$page][2]{ocr_flag};
  5267.   my $ocr_time   = $slist->{data}[$page][2]{ocr_time};
  5268.   $dirty_time = defined($dirty_time) ? $dirty_time : 0;
  5269.   $ocr_time   = defined($ocr_time)   ? $ocr_time   : 0;
  5270.   push @selection, $_ if ( $ocr_flag and ( $ocr_time le $dirty_time ) );
  5271.  }
  5272.  
  5273.  $slist->get_selection->unselect_all;
  5274.  $slist->select(@selection);
  5275.  return;
  5276. }
  5277.  
  5278. # Select pages with no ocr output
  5279.  
  5280. sub select_no_ocr {
  5281.  my @selection;
  5282.  for ( 0 .. $#{ $slist->{data} } ) {
  5283.   push @selection, $_ unless ( defined $slist->{data}[$_][2]{hocr} );
  5284.  }
  5285.  
  5286.  $slist->get_selection->unselect_all;
  5287.  $slist->select(@selection);
  5288.  return;
  5289. }
  5290.  
  5291. # Clear the OCR output from selected pages
  5292.  
  5293. sub clear_ocr {
  5294.  
  5295.  # Update undo/redo buffers
  5296.  take_snapshot();
  5297.  
  5298.  # Remove the existing canvas
  5299.  $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
  5300.  
  5301.  my @selection = $slist->get_selected_indices;
  5302.  for (@selection) {
  5303.   delete $slist->{data}[$_][2]{hocr};
  5304.  }
  5305.  return;
  5306. }
  5307.  
  5308. # Analyse and select blank pages
  5309.  
  5310. sub analyse_select_blank {
  5311.  analyse( 1, 0 );
  5312.  return;
  5313. }
  5314.  
  5315. # Select blank pages
  5316.  
  5317. sub select_blank_pages {
  5318.  $slist->get_selection->unselect_all;
  5319.  foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  5320.   $logger->info("smp page: $page");
  5321.   my $dirty_time   = $slist->{data}[$page][2]{dirty_time};
  5322.   my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  5323.   $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  5324.   $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  5325.   if ( $analyse_time le $dirty_time ) {
  5326.    $logger->warn(
  5327.     $d->get(
  5328.      sprintf(
  5329.       "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
  5330.       $page + 1 )
  5331.     )
  5332.    );
  5333.    next;
  5334.   }
  5335.  
  5336.   #compare Std Dev to threshold
  5337.   if ( $slist->{data}[$page][2]{std_dev} <= $SETTING{'Blank threshold'} ) {
  5338.    $slist->select($page);
  5339.    $logger->info("Selecting blank page");
  5340.   }
  5341.   $logger->info( "StdDev: "
  5342.      . $slist->{data}[$page][2]{std_dev}
  5343.      . " threshold: "
  5344.      . $SETTING{'Blank threshold'} );
  5345.  }
  5346.  return;
  5347. }
  5348.  
  5349. # Analyse and select dark pages
  5350.  
  5351. sub analyse_select_dark {
  5352.  analyse( 0, 1 );
  5353.  return;
  5354. }
  5355.  
  5356. # Select dark pages
  5357.  
  5358. sub select_dark_pages {
  5359.  $slist->get_selection->unselect_all;
  5360.  $logger->info("Checking for dark pages now, is analysis done?");
  5361.  foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  5362.   $logger->info("smp page: $page");
  5363.   my $dirty_time   = $slist->{data}[$page][2]{dirty_time};
  5364.   my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  5365.   $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  5366.   $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  5367.   if ( $analyse_time le $dirty_time ) {
  5368.    $logger->warn(
  5369.     $d->get(
  5370.      sprintf(
  5371.       "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
  5372.       $page + 1 )
  5373.     )
  5374.    );
  5375.    next;
  5376.   }
  5377.  
  5378.   #compare Mean to threshold
  5379.   if ( $slist->{data}[$page][2]{mean} <= $SETTING{'Dark threshold'} ) {
  5380.    $slist->select($page);
  5381.    $logger->info("Selecting dark page");
  5382.   }
  5383.  }
  5384.  return;
  5385. }
  5386.  
  5387. # Display about dialog
  5388.  
  5389. sub about {
  5390.  use utf8;
  5391.  my $about = Gtk2::AboutDialog->new;
  5392.  
  5393.  # Gtk2::AboutDialog->set_url_hook ($func, $data=undef);
  5394.  # Gtk2::AboutDialog->set_email_hook ($func, $data=undef);
  5395.  $about->set_program_name($prog_name);
  5396.  $about->set_version($version);
  5397.  my $authors = <<'EOS';
  5398. Frederik Elwert
  5399. Klaus Ethgen
  5400. Andy Fingerhut
  5401. Leon Fisk
  5402. John Goerzen
  5403. David Hampton
  5404. Sascha Hunold
  5405. Matthijs Kooijman
  5406. Chris Mayo
  5407. Torsten Schönfeld
  5408. Roy Shahbazian
  5409. Jarl Stefansson
  5410. Jakub Wilk
  5411. EOS
  5412.  $about->set_authors( "Jeff Ratcliffe\n\n"
  5413.     . $d->get('Patches gratefully received from:')
  5414.     . "\n$authors" );
  5415.  $about->set_comments( $d->get('To aid the scan-to-PDF process') );
  5416.  $about->set_copyright( $d->get('Copyright 2006--2013 Jeffrey Ratcliffe') );
  5417.  my $licence = <<'EOS';
  5418. gscan2pdf --- to aid the scan to PDF or DjVu process
  5419. Copyright 2006 -- 2013 Jeffrey Ratcliffe <[email protected]>
  5420.  
  5421. This program is free software: you can redistribute it and/or modify
  5422. it under the terms of the version 3 GNU General Public License as
  5423. published by the Free Software Foundation.
  5424.  
  5425. This program is distributed in the hope that it will be useful,
  5426. but WITHOUT ANY WARRANTY; without even the implied warranty of
  5427. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  5428. GNU General Public License for more details.
  5429.  
  5430. You should have received a copy of the GNU General Public License
  5431. along with this program.  If not, see <http://www.gnu.org/licenses/>.
  5432. EOS
  5433.  $about->set_license($licence);
  5434.  $about->set_website('http://gscan2pdf.sf.net');
  5435.  my $translators =
  5436.    <<'EOS'; # inverted commas required around EOS because of UTF-8 in $translators
  5437. Constantine Mousafiris
  5438. Shushi Kurose
  5439. vishal goswami
  5440. Prescott_SK
  5441. Diego Verga
  5442. Herczeg Zsolt
  5443. stavros kakaliantis
  5444. Zbyněk Schwarz
  5445. Wang Dianjin
  5446. Wojciech Górnaś
  5447. Arthur Rodrigues
  5448. Utku BERBEROĞLU
  5449. Dennis Baudys
  5450. tzem
  5451. Daniel Dietrich
  5452. Daniel
  5453. Adolfo Jayme Barrientos
  5454. Christoph Steinbrink
  5455. Igor Zubarev
  5456. EOS
  5457.  $about->set_translator_credits($translators);
  5458.  $about->set_artists('lodp');
  5459.  $about->run;
  5460.  $about->destroy;
  5461.  return;
  5462. }
  5463.  
  5464. # Check that a command exists
  5465.  
  5466. sub check_command {
  5467.  return system("which $_[0] >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
  5468. }
  5469.  
  5470. # Rescan device-dependent scan options
  5471.  
  5472. sub rescan_options {
  5473.  my ($device) = @_;
  5474.  
  5475.  #  Ghost the scan button whilst options being updated
  5476.  $scan_button->set_sensitive(FALSE) if ( defined $scan_button );
  5477.  
  5478.  # Empty $vboxd first
  5479.  foreach ( $vboxd->get_children ) {
  5480.   $_->destroy;
  5481.  }
  5482.  
  5483.  # Get the options from the cache if defined
  5484.  my $mode;
  5485.  if ( defined $SETTING{mode} ) {
  5486.   $mode = $SETTING{mode};
  5487.  }
  5488.  else {
  5489.   $mode = 'default';
  5490.  }
  5491.  if ( $SETTING{'cache options'} and defined $SETTING{cache}{$device}{$mode} ) {
  5492.   my $options  = options2hash( $SETTING{cache}{$device}{$mode} );
  5493.   my $response = 'cancel';
  5494.   unless ( $options->num_options ) {
  5495.    $response = show_message_dialog(
  5496.     $window,
  5497.     'question',
  5498.     'ok-cancel',
  5499.     $d->get(
  5500.      "The options cache seems to be empty. Reload device-dependent options?")
  5501.    );
  5502.   }
  5503.   if ( $response eq 'cancel' ) {
  5504.    parse_options( $device, $options );
  5505.    return;
  5506.   }
  5507.  }
  5508.  
  5509.  if ($test) {
  5510.   my $i = 0;
  5511.   $i++ while ( $device[$i] ne $device );
  5512.  
  5513.   # check if we have output for the mode
  5514.   my $filename = $test->{file}[$i];
  5515.   $filename .= ".$SETTING{mode}"
  5516.     if ( defined( $SETTING{mode} ) and -e "$filename.$SETTING{mode}" );
  5517.  
  5518.   # Slurp it from file
  5519.   my $output = Gscan2pdf::Document::slurp($filename);
  5520.   $vboxd->hide_all;    # merely here for consistency with normal operation
  5521.   parse_options( $device, options2hash($output) );
  5522.  }
  5523.  else {
  5524.   # Set up ProgressBar
  5525.   my $pbar = Gtk2::ProgressBar->new;
  5526.   $pbar->set_pulse_step(.1);
  5527.   $pbar->set_text( $d->get('Updating options') );
  5528.   $vboxd->pack_start( $pbar, FALSE, FALSE, 0 );
  5529.   $vboxd->show_all;
  5530.  
  5531.   Gscan2pdf::Frontend::CLI->find_scan_options(
  5532.    prefix           => $SETTING{'scan prefix'},
  5533.    frontend         => $SETTING{frontend},
  5534.    device           => $device,
  5535.    mode             => $SETTING{mode},
  5536.    running_callback => sub {
  5537.     $pbar->pulse;
  5538.    },
  5539.    finished_callback => sub {
  5540.     my ($output) = @_;
  5541.     $pbar->destroy;
  5542.     $vboxd->hide_all;
  5543.     $logger->info($output);
  5544.     parse_options( $device, options2hash($output) );
  5545.    },
  5546.    error_callback => sub {
  5547.     my ($message) = @_;
  5548.     show_message_dialog( $window, 'error', 'close', $message );
  5549.     $pbar->destroy;
  5550.     $vboxd->hide_all;
  5551.     $logger->warn($message);
  5552.    },
  5553.   );
  5554.  }
  5555.  return;
  5556. }
  5557.  
  5558. sub parse_options {
  5559.  my ( $device, $options ) = @_;
  5560.  
  5561.  # Dig out the paper sizes
  5562.  my (
  5563.   $x,            $y,            $l,            $t,
  5564.   $h,            $w,            $spin_buttonx, $spin_buttony,
  5565.   $spin_buttonl, $spin_buttont, $spin_buttonh, $spin_buttonw,
  5566.  );
  5567.  $x = $options->by_name('x')->{constraint}{max}
  5568.    if ( defined $options->by_name('x') );
  5569.  $y = $options->by_name('y')->{constraint}{max}
  5570.    if ( defined $options->by_name('y') );
  5571.  $l = $options->by_name('l')->{constraint}{max}
  5572.    if ( defined $options->by_name('l') );
  5573.  $t = $options->by_name('t')->{constraint}{max}
  5574.    if ( defined $options->by_name('t') );
  5575.  
  5576.  # Set device-dependent options
  5577.  # Dummy entries so that something is returned:
  5578.  %ddo = (
  5579.   'Paper size' => { string => $d->get('Paper size'), },
  5580.   'x'          => { string => $d->get('Width') },
  5581.   'y'          => { string => $d->get('Height') },
  5582.   'l'          => { string => $d->get('Left') },
  5583.   't'          => { string => $d->get('Top') },
  5584.  );
  5585.  
  5586.  if ( defined($x) and defined($y) ) {
  5587.  
  5588.   # HBox for paper size
  5589.   my $hboxp = Gtk2::HBox->new;
  5590.   $vboxd->pack_start( $hboxp, FALSE, FALSE, 0 );
  5591.  
  5592.   # Paper list
  5593.   my $labelp = Gtk2::Label->new( $ddo{'Paper size'}{string} );
  5594.   $hboxp->pack_start( $labelp, FALSE, FALSE, 0 );
  5595.  
  5596.   $combobp = Gtk2::ComboBox->new_text;
  5597.   $combobp->append_text( $d->get('Manual') );
  5598.   $combobp->append_text( $d->get('Edit') );
  5599.   $tooltips->set_tip( $combobp, $d->get('Selects or edits the paper size') );
  5600.   $hboxp->pack_end( $combobp, FALSE, FALSE, 0 );
  5601.  
  5602.   # Define manual paper here to reference it in callback
  5603.   $vbox_manual_paper = Gtk2::VBox->new;
  5604.   $vboxd->pack_start( $vbox_manual_paper, FALSE, FALSE, 0 );
  5605.  }
  5606.  
  5607.  # Add remaining options
  5608.  for ( 0 .. $options->num_options - 1 ) {
  5609.   my $option = $options->by_index($_);
  5610.   if ( defined( $pddo{ $option->{name} } ) ) {
  5611.    my ( $widget, $hbox ) = add_widget_from_hash($option);
  5612.    if ( $option->{name} =~ /^page-?width$/x ) {
  5613.     $w            = $option->{constraint}{max};
  5614.     $spin_buttonw = $widget;
  5615.     $vbox_manual_paper->pack_start( $hbox, TRUE, TRUE, 0 );
  5616.    }
  5617.    elsif ( $option->{name} =~ /^page-?height$/x ) {
  5618.     $h            = $option->{constraint}{max};
  5619.     $spin_buttonh = $widget;
  5620.     $vbox_manual_paper->pack_start( $hbox, TRUE, TRUE, 0 );
  5621.    }
  5622.    else {
  5623.     $vboxd->pack_start( $hbox, TRUE, TRUE, 0 );
  5624.    }
  5625.    $batch_scan = $widget if ( $option->{name} eq 'batch-scan' );
  5626.  
  5627.    # If an ADF isn't selected, then we don't want to scan all pages
  5628.    if ( $option->{name} eq 'source' ) {
  5629.     $widget->signal_connect(
  5630.      changed => sub {
  5631.       my $text = $widget->get_active_text;
  5632.       if ( defined($text)
  5633.        and get_value( \%ddo, $option->{name}, $text ) =~ /(flatbed|normal)/ix )
  5634.       {
  5635.        $bscanall->set_sensitive(FALSE);
  5636.        $bscannum->set_active(TRUE);
  5637.       }
  5638.       else {
  5639.        $bscanall->set_sensitive(TRUE);
  5640.        if ( defined($text)
  5641.         and get_value( \%ddo, $option->{name}, $text ) =~
  5642.         /(ADF|Automatic\ Document\ Feeder)/ix )
  5643.        {
  5644.         $batch_scan->set_active(0) if ( defined $batch_scan );
  5645.        }
  5646.       }
  5647.       if ( defined($text)
  5648.        and get_value( \%ddo, $option->{name}, $text ) =~ /duplex/i )
  5649.       {
  5650.        $duplex = TRUE;
  5651.        $frames->set_sensitive(FALSE);
  5652.       }
  5653.       else {
  5654.        $duplex = FALSE;
  5655.        $frames->set_sensitive(TRUE);
  5656.       }
  5657.      }
  5658.     );
  5659.    }
  5660.  
  5661.    # If the mode is changed, update the options
  5662.    if ( $option->{name} eq 'mode' ) {
  5663.     $widget->signal_connect(
  5664.      changed => sub {
  5665.       $SETTING{ $option->{name} } =
  5666.         get_value( \%ddo, $option->{name}, $widget->get_active_text );
  5667.       update_options($device);
  5668.      }
  5669.     );
  5670.    }
  5671.   }
  5672.  }
  5673.  
  5674.  # Add paper size after rest of options in case of pagewidth|height
  5675.  if ( defined($x) and defined($y) ) {
  5676.   $x = $w if ( defined $w );
  5677.   $y = $h if ( defined $h );
  5678.   my @info = (
  5679.    {
  5680.     label     => $d->get('Width'),
  5681.     tooltip   => $d->get('Width of scan area'),
  5682.     letter    => 'x',
  5683.     dimension => $x
  5684.    },
  5685.    {
  5686.     label     => $d->get('Height'),
  5687.     tooltip   => $d->get('Height of scan area'),
  5688.     letter    => 'y',
  5689.     dimension => $y
  5690.    },
  5691.    {
  5692.     label     => $d->get('Left'),
  5693.     tooltip   => $d->get('Top-left x position of scan area'),
  5694.     letter    => 'l',
  5695.     dimension => $l
  5696.    },
  5697.    {
  5698.     label     => $d->get('Top'),
  5699.     tooltip   => $d->get('Top-left y position of scan area'),
  5700.     letter    => 't',
  5701.     dimension => $t
  5702.    }
  5703.   );
  5704.   ( $spin_buttonx, $spin_buttony, $spin_buttonl, $spin_buttont ) =
  5705.     add_spinbutton( $vbox_manual_paper, @info );
  5706.   $combobp->signal_connect(
  5707.    changed => sub {
  5708.     if ( $combobp->get_active_text eq $d->get('Edit') ) {
  5709.      edit_paper($options);
  5710.     }
  5711.     elsif ( $combobp->get_active_text eq $d->get('Manual') ) {
  5712.      $vbox_manual_paper->show_all;
  5713.     }
  5714.     else {
  5715.      my $paper = $combobp->get_active_text;
  5716.      $spin_buttonx->set_value( $SETTING{Paper}{$paper}{x} );
  5717.      $spin_buttony->set_value( $SETTING{Paper}{$paper}{y} );
  5718.      $spin_buttont->set_value( $SETTING{Paper}{$paper}{t} );
  5719.      $spin_buttonl->set_value( $SETTING{Paper}{$paper}{l} );
  5720.      if ( defined($spin_buttonh) and defined($spin_buttonw) ) {
  5721.       $spin_buttonw->set_value( $SETTING{Paper}{$paper}{x} );
  5722.       $spin_buttonh->set_value( $SETTING{Paper}{$paper}{y} );
  5723.      }
  5724.      Glib::Idle->add( sub { $vbox_manual_paper->hide; } );
  5725.     }
  5726.    }
  5727.   );
  5728.   add_paper( $combobp, $options );
  5729.  }
  5730.  
  5731.  # Show window
  5732.  $vboxd->show_all;
  5733.  
  5734.  # Give the GUI a chance to catch up before resizing.
  5735.  Glib::Idle->add( sub { $windows->resize( 100, 100 ); } );
  5736.  Gscan2pdf::Dialog::Scan::Sane::set_combobox_by_text( $combobp,
  5737.   $SETTING{'Paper size'} );
  5738.  
  5739.  #  Unghost the scan button
  5740.  if ( defined $scan_button ) {
  5741.   $scan_button->set_sensitive(TRUE);
  5742.   $scan_button->grab_focus;
  5743.  }
  5744.  return;
  5745. }
  5746.  
  5747. # take an option from the options hash and create the appropriate widget
  5748.  
  5749. sub add_widget_from_hash {
  5750.  my ($option) = @_;
  5751.  
  5752.  # Dig out of possible options
  5753.  $ddo{ $option->{name} }{string} = $pddo{ $option->{name} }{string};
  5754.  
  5755.  # HBox for option
  5756.  my $hbox = Gtk2::HBox->new;
  5757.  $hbox->set_sensitive(FALSE) if ( $option->{default} =~ /inactive/ );
  5758.  
  5759.  # Label
  5760.  my $label = Gtk2::Label->new( $ddo{ $option->{name} }{string} );
  5761.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  5762.  
  5763.  # Widget
  5764.  my $widget;
  5765.  
  5766.  # SpinButton
  5767.  if ( defined $option->{constraint}{max} ) {
  5768.   my $step = 1;
  5769.   $step = $option->{constraint}{step}
  5770.     if ( defined $option->{constraint}{step} );
  5771.   $widget = Gtk2::SpinButton->new_with_range( $option->{constraint}{min},
  5772.    $option->{constraint}{max}, $step );
  5773.  
  5774.   # Set the default
  5775.   $widget->set_value( $option->{default} )
  5776.     unless ( $option->{default} =~ /inactive/ );
  5777.  }
  5778.  
  5779.  # ComboBox
  5780.  else {
  5781.   $widget = Gtk2::ComboBox->new_text;
  5782.  
  5783.   my $index = default2index( $option->{default}, @{ $option->{values} } );
  5784.   foreach ( @{ $option->{values} } ) {
  5785.    add_to_options( $option->{name}, $_ );
  5786.    $widget->append_text( $ddo{ $option->{name} }{values}{$_} );
  5787.   }
  5788.  
  5789.   # Set the default
  5790.   $widget->set_active($index) if ( defined $index );
  5791.  }
  5792.  
  5793.  $hbox->pack_end( $widget, FALSE, FALSE, 0 );
  5794.  $tooltips->set_tip( $widget, $option->{tip} );
  5795.  return ( $widget, $hbox );
  5796. }
  5797.  
  5798. # Helper function to get manual paper size spinbuttons on dialog
  5799.  
  5800. sub add_spinbutton {
  5801.  my ( $vbox, @info ) = @_;
  5802.  my @stack;
  5803.  for (@info) {
  5804.   my $hbox = Gtk2::HBox->new;
  5805.   $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
  5806.   my $label = Gtk2::Label->new( $_->{label} );
  5807.   $hbox->pack_start( $label, FALSE, FALSE, 0 );
  5808.   my $spinbutton = Gtk2::SpinButton->new_with_range( 0, $_->{dimension}, 1 );
  5809.   $SETTING{ $_->{letter} } = $_->{dimension}
  5810.     unless ( defined $SETTING{ $_->{letter} } );
  5811.   $spinbutton->set_value( $SETTING{ $_->{letter} } );
  5812.   $tooltips->set_tip( $spinbutton, $_->{tooltip} );
  5813.   $hbox->pack_end( $spinbutton, FALSE, FALSE, 0 );
  5814.   push @stack, $spinbutton;
  5815.  }
  5816.  return @stack;
  5817. }
  5818.  
  5819. # Paper editor
  5820. sub edit_paper {
  5821.  my ($options) = @_;
  5822.  
  5823.  if ( defined $windowd ) {
  5824.   $windowd->present;
  5825.   return;
  5826.  }
  5827.  
  5828.  $windowd = Gscan2pdf::Dialog->new(
  5829.   'transient-for' => $windows,
  5830.   title           => $d->get('Edit paper size'),
  5831.   border_width    => $border_width
  5832.  );
  5833.  my $vbox1 = $windowd->get('vbox');
  5834.  
  5835.  # Buttons for SimpleList
  5836.  my $hboxl = Gtk2::HBox->new;
  5837.  $vbox1->pack_start( $hboxl, FALSE, FALSE, 0 );
  5838.  my $vboxb = Gtk2::VBox->new;
  5839.  $hboxl->pack_start( $vboxb, FALSE, FALSE, 0 );
  5840.  my $dbutton = Gtk2::Button->new_from_stock('gtk-add');
  5841.  $vboxb->pack_start( $dbutton, TRUE, FALSE, 0 );
  5842.  my $rbutton = Gtk2::Button->new_from_stock('gtk-remove');
  5843.  $vboxb->pack_end( $rbutton, TRUE, FALSE, 0 );
  5844.  
  5845.  # Set up a SimpleList
  5846.  my $paper_list = Gtk2::Ex::Simple::List->new(
  5847.   $d->get('Name')   => 'text',
  5848.   $d->get('Width')  => 'int',
  5849.   $d->get('Height') => 'int',
  5850.   $d->get('Left')   => 'int',
  5851.   $d->get('Top')    => 'int'
  5852.  );
  5853.  for ( keys %{ $SETTING{Paper} } ) {
  5854.   push @{ $paper_list->{data} },
  5855.     [
  5856.    $_,                     $SETTING{Paper}{$_}{x}, $SETTING{Paper}{$_}{y},
  5857.    $SETTING{Paper}{$_}{l}, $SETTING{Paper}{$_}{t}
  5858.     ];
  5859.  }
  5860.  
  5861.  # Set everything to be editable
  5862.  for ( 0 .. 4 ) {
  5863.   $paper_list->set_column_editable( $_, TRUE );
  5864.  }
  5865.  $paper_list->get_column(0)->set_sort_column_id(0);
  5866.  
  5867.  # Add button callback
  5868.  $dbutton->signal_connect(
  5869.   clicked => sub {
  5870.    my @rows = $paper_list->get_selected_indices;
  5871.    $rows[0] = 0 unless (@rows);
  5872.    my $name          = $paper_list->{data}[ $rows[0] ][0];
  5873.    my $paper_version = 2;
  5874.    my $i             = 0;
  5875.    while ( $i < @{ $paper_list->{data} } ) {
  5876.     if ( $paper_list->{data}[$i][0] eq "$name ($paper_version)" ) {
  5877.      ++$paper_version;
  5878.      $i = 0;
  5879.     }
  5880.     else {
  5881.      ++$i;
  5882.     }
  5883.    }
  5884.    my @line = [
  5885.     "$name ($paper_version)",
  5886.     $paper_list->{data}[ $rows[0] ][1],
  5887.     $paper_list->{data}[ $rows[0] ][2],
  5888.     $paper_list->{data}[ $rows[0] ][3],
  5889.     $paper_list->{data}[ $rows[0] ][4]
  5890.    ];
  5891.    splice @{ $paper_list->{data} }, $rows[0] + 1, 0, @line;
  5892.   }
  5893.  );
  5894.  
  5895.  # Remove button callback
  5896.  $rbutton->signal_connect(
  5897.   clicked => sub {
  5898.    my @rows = $paper_list->get_selected_indices;
  5899.    if ( $#rows == $#{ $paper_list->{data} } ) {
  5900.     show_message_dialog( $windowd, 'error', 'close',
  5901.      $d->get('Cannot delete all paper sizes') );
  5902.    }
  5903.    else {
  5904.     while (@rows) {
  5905.      splice @{ $paper_list->{data} }, shift(@rows), 1;
  5906.     }
  5907.    }
  5908.   }
  5909.  );
  5910.  
  5911.  # Set-up the callback to check that no two Names are the same
  5912.  $paper_list->get_model->signal_connect(
  5913.   'row-changed' => sub {
  5914.    my ( $model, $path, $iter ) = @_;
  5915.    for ( my $i = 0 ; $i < @{ $paper_list->{data} } ; $i++ ) {
  5916.     if ( $i != $path->to_string
  5917.      and $paper_list->{data}[ $path->to_string ][0] eq
  5918.      $paper_list->{data}[$i][0] )
  5919.     {
  5920.      my $name          = $paper_list->{data}[ $path->to_string ][0];
  5921.      my $paper_version = 2;
  5922.      if ( $name =~ /(.*)\ \((\d+)\)/x ) {
  5923.       $name          = $1;
  5924.       $paper_version = $2 + 1;
  5925.      }
  5926.      $paper_list->{data}[ $path->to_string ][0] = "$name ($paper_version)";
  5927.      return;
  5928.     }
  5929.    }
  5930.   }
  5931.  );
  5932.  $hboxl->pack_end( $paper_list, FALSE, FALSE, 0 );
  5933.  
  5934.  # Buttons
  5935.  my $hboxb = _hbox_with_apply_cancel_buttons(
  5936.   sub {
  5937.    delete $SETTING{Paper};
  5938.    for ( my $i = 0 ; $i < @{ $paper_list->{data} } ; $i++ ) {
  5939.     $SETTING{Paper}{ $paper_list->{data}[$i][0] }{x} =
  5940.       $paper_list->{data}[$i][1];
  5941.     $SETTING{Paper}{ $paper_list->{data}[$i][0] }{y} =
  5942.       $paper_list->{data}[$i][2];
  5943.     $SETTING{Paper}{ $paper_list->{data}[$i][0] }{l} =
  5944.       $paper_list->{data}[$i][3];
  5945.     $SETTING{Paper}{ $paper_list->{data}[$i][0] }{t} =
  5946.       $paper_list->{data}[$i][4];
  5947.    }
  5948.  
  5949.    # Update list in Gscan2pdf::Document so that it can be used by get_resolution
  5950.    # FIXME: make sure that this is updated for all scan dialogs
  5951.    Gscan2pdf::Document->set_paper_sizes( $SETTING{Paper} );
  5952.  
  5953.    $combobp->remove_text(0) while ( $combobp->get_active > 1 );
  5954.    my @ignored = add_paper( $combobp, $options );
  5955.    show_message_dialog(
  5956.     $windowd,
  5957.     'warning',
  5958.     'close',
  5959.     $d->get(
  5960. 'The following paper sizes are too big to be scanned by the selected device:'
  5961.       )
  5962.       . ' '
  5963.       . join( ', ', @ignored )
  5964.    ) if (@ignored);
  5965.    my @rows = $paper_list->get_selected_indices;
  5966.    $rows[0] = 0 unless (@rows);
  5967.    $SETTING{'Paper size'} = $paper_list->{data}[ $rows[0] ][0];
  5968.    Gscan2pdf::Dialog::Scan::Sane::set_combobox_by_text( $combobp,
  5969.     $SETTING{'Paper size'} );
  5970.    $windowd->hide;
  5971.   },
  5972.   sub {
  5973.    my @rows = $paper_list->get_selected_indices;
  5974.    $rows[0] = 0 unless (@rows);
  5975.    $SETTING{'Paper size'} = $paper_list->{data}[ $rows[0] ][0];
  5976.    Gscan2pdf::Dialog::Scan::Sane::set_combobox_by_text( $combobp,
  5977.     $SETTING{'Paper size'} );
  5978.    $windowd->hide;
  5979.   }
  5980.  );
  5981.  $vbox1->pack_start( $hboxb, FALSE, FALSE, 0 );
  5982.  $windowd->show_all;
  5983.  return;
  5984. }
  5985.  
  5986. # Add paper size to combobox if scanner large enough
  5987.  
  5988. sub add_paper {
  5989.  my ( $combobox, $options ) = @_;
  5990.  my $tolerance = 1;
  5991.  my (@ignored);
  5992.  for ( keys %{ $SETTING{Paper} } ) {
  5993.   if ( $options->supports_paper( $SETTING{Paper}{$_}, $tolerance ) ) {
  5994.    $combobox->prepend_text($_);
  5995.   }
  5996.   else {
  5997.    push @ignored, $_;
  5998.   }
  5999.  }
  6000.  return @ignored;
  6001. }
  6002.  
  6003. # Update device-dependent scan options having selected a new mode
  6004.  
  6005. sub update_options {
  6006.  my ($device) = @_;
  6007.  
  6008.  # Empty $vboxd first
  6009.  foreach ( $vboxd->get_children ) {
  6010.   $_->hide;
  6011.  }
  6012.  
  6013.  my $mode;
  6014.  if ( defined $SETTING{mode} ) {
  6015.   $mode = $SETTING{mode};
  6016.  }
  6017.  else {
  6018.   $mode = 'default';
  6019.  }
  6020.  if ( $SETTING{'cache options'} and defined $SETTING{cache}{$device}{$mode} ) {
  6021.   my $options  = options2hash( $SETTING{cache}{$device}{$mode} );
  6022.   my $response = 'cancel';
  6023.   unless ( $options->num_options ) {
  6024.    $response = show_message_dialog(
  6025.     $window,
  6026.     'question',
  6027.     'ok-cancel',
  6028.     $d->get(
  6029.      "The options cache seems to be empty. Reload device-dependent options?")
  6030.    );
  6031.   }
  6032.   if ( $response eq 'cancel' ) {
  6033.    update_options_hash($options);
  6034.    return;
  6035.   }
  6036.  }
  6037.  
  6038.  if ($test) {
  6039.   my $i = 0;
  6040.   $i++ while ( $device[$i] ne $device );
  6041.  
  6042.   # check if we have output for the new mode
  6043.   my $filename = $test->{file}[$i];
  6044.   $filename .= ".$SETTING{mode}" if ( -e "$filename.$SETTING{mode}" );
  6045.  
  6046.   # Slurp it from file
  6047.   my $output = Gscan2pdf::Document::slurp($filename);
  6048.   $vboxd->hide_all;    # merely here for consistency with normal operation
  6049.   update_options_hash( options2hash($output) );
  6050.  }
  6051.  else {
  6052.   # Set up ProgressBar
  6053.   my $pbar = Gtk2::ProgressBar->new;
  6054.   $pbar->set_pulse_step(.1);
  6055.   $pbar->set_text( $d->get('Updating options') );
  6056.   $vboxd->pack_start( $pbar, FALSE, FALSE, 0 );
  6057.   $pbar->show;
  6058.  
  6059.   Gscan2pdf::Frontend::CLI->find_scan_options(
  6060.    prefix           => $SETTING{'scan prefix'},
  6061.    frontend         => $SETTING{frontend},
  6062.    device           => $device,
  6063.    mode             => $SETTING{mode},
  6064.    running_callback => sub {
  6065.     $pbar->pulse;
  6066.    },
  6067.    finished_callback => sub {
  6068.     my ($output) = @_;
  6069.     $pbar->destroy;
  6070.     $vboxd->hide_all;
  6071.     $logger->info($output);
  6072.     update_options_hash( options2hash($output) );
  6073.    }
  6074.   );
  6075.  }
  6076.  return;
  6077. }
  6078.  
  6079. # return the options, updated with the user defaults
  6080.  
  6081. sub options2hash {
  6082.  
  6083.  my ($output) = @_;
  6084.  my $options = Gscan2pdf::Scanner::Options->new_from_data($output);
  6085.  $logger->debug( Dumper($options) );
  6086.  for ( 0 .. $options->num_options - 1 ) {
  6087.   my $option = $options->by_index($_);
  6088.  
  6089.   # Set default from config
  6090.   if (
  6091.    defined( $SETTING{ $option->{name} } )
  6092.    and $option->{default} ne 'inactive'
  6093.  
  6094.    # only set the default if it is an option
  6095.    and (
  6096.     defined( $option->{constraint}{max} )
  6097.     or defined(
  6098.      default2index( $SETTING{ $option->{name} }, @{ $option->{values} } )
  6099.     )
  6100.    )
  6101.     )
  6102.   {
  6103.    $option->{default} = $SETTING{ $option->{name} };
  6104.   }
  6105.  
  6106.   # in the case that we've switched devices and the new device doesn't support
  6107.   # the new mode
  6108.   elsif ( $option->{name} eq 'mode' ) {
  6109.    delete $SETTING{ $option->{name} };
  6110.   }
  6111.  }
  6112.  
  6113.  # Cache options
  6114.  my $mode;
  6115.  if ( defined $SETTING{mode} ) {
  6116.   $mode = $SETTING{mode};
  6117.  }
  6118.  else {
  6119.   $mode = 'default';
  6120.  }
  6121.  
  6122.  # We could cache the hash, but we'd have to turn off MergeDuplicateOptions
  6123.  $SETTING{cache}{ $SETTING{device} }{$mode} = $output
  6124.    if ( $SETTING{'cache options'} and defined $SETTING{device} );
  6125.  
  6126.  return $options;
  6127. }
  6128.  
  6129. # walk the widget tree and update them from the hash
  6130.  
  6131. sub update_options_hash {
  6132.  
  6133.  my ($options) = @_;
  6134.  my ( $hboxm, $hboxp );
  6135.  
  6136.  foreach my $hbox ( $vboxd->get_children ) {
  6137.   my $key;
  6138.   if ( $hbox->isa('Gtk2::HBox') ) {
  6139.    foreach my $widget ( $hbox->get_children ) {
  6140.     my $opt;
  6141.     $opt = $options->by_name($key) if ( defined $key );
  6142.     if ( $widget->isa('Gtk2::Label') ) {  ## no critic (ProhibitCascadingIfElse)
  6143.      $key = get_key( \%ddo, $widget->get_label );
  6144.     }
  6145.     elsif (
  6146.      $widget->isa('Gtk2::ComboBox')
  6147.  
  6148.      # to prevent recursion
  6149.      and $key ne 'mode'
  6150.      and $key ne 'Paper size'
  6151.      and defined($opt)
  6152.      and defined( $opt->{values} )
  6153.       )
  6154.     {
  6155.  
  6156.      # Empty the list
  6157.      $widget->get_model->clear;
  6158.  
  6159.      # Fill it again
  6160.      my $index = default2index( $opt->{val}, @{ $opt->{constraint} } );
  6161.      foreach ( @{ $opt->{constraint} } ) {
  6162.  
  6163.       # in case the option was new and therefore not in ddo before
  6164.       add_to_options( $key, $_ );
  6165.       $widget->append_text( $ddo{$key}{values}{$_} );
  6166.      }
  6167.  
  6168.      # Set the default
  6169.      $widget->set_active($index) if ( defined $index );
  6170.  
  6171.      # Set the ghosting
  6172.      if ( $opt->{val} =~ /inactive/ ) {
  6173.       $hbox->set_sensitive(FALSE);
  6174.      }
  6175.      else {
  6176.       $hbox->set_sensitive(TRUE);
  6177.      }
  6178.  
  6179.      $hbox->show_all;
  6180.     }
  6181.     elsif ( $key eq 'mode' ) {
  6182.      $hboxm = $hbox;
  6183.     }
  6184.     elsif ( $key eq 'Paper size' ) {
  6185.      $hboxp = $hbox;
  6186.     }
  6187.     elsif ( $widget->isa('Gtk2::SpinButton')
  6188.      and defined($opt)
  6189.      and defined( $opt->{constraint}{max} ) )
  6190.     {
  6191.      $widget->set_range( $opt->{constraint}{min}, $opt->{constraint}{max} );
  6192.  
  6193.      # Set the default
  6194.      if ( $opt->{val} =~ /inactive/ ) {
  6195.       $hbox->set_sensitive(FALSE);
  6196.      }
  6197.      else {
  6198.       $widget->set_value( $opt->{val} );
  6199.       $hbox->set_sensitive(TRUE);
  6200.      }
  6201.      $hbox->show_all;
  6202.     }
  6203.  
  6204.     # If an option no longer available, make it insensitive so that it doesn't
  6205.     # get passed to scanimage or scanadf
  6206.     else {
  6207.      $hbox->set_sensitive(FALSE);
  6208.     }
  6209.    }
  6210.  
  6211.    # Delete option from hash to see at the end which are left
  6212.    $options->delete_by_name($key) if ( defined $options->by_name($key) );
  6213.   }
  6214.  }
  6215.  
  6216.  # Add any new options
  6217.  for ( 0 .. $options->num_options - 1 ) {
  6218.   my $option = $options->by_index($_);
  6219.   if (
  6220.        defined($option)
  6221.    and defined( $pddo{ $option->{name} } )
  6222.  
  6223.    and $option->{name} !~ /^page-?(?:width|height)/x
  6224.     )
  6225.   {
  6226.    my ( $widget, $hbox ) = add_widget_from_hash($option);
  6227.    $vboxd->pack_start( $hbox, TRUE, TRUE, 0 );
  6228.    $hbox->show_all;
  6229.   }
  6230.  }
  6231.  
  6232.  $vbox_manual_paper->show_all
  6233.    if ( $combobp->get_active_text eq $d->get('Manual') );
  6234.  $hboxm->show_all if ( defined $hboxm );
  6235.  $hboxp->show_all if ( defined $hboxp );
  6236.  $vboxd->show;
  6237.  return;
  6238. }
  6239.  
  6240. # return the position that a string occurs in an array
  6241.  
  6242. sub default2index {
  6243.  
  6244.  my ( $default, @array ) = @_;
  6245.  for ( my $i = 0 ; $i < @array ; $i++ ) {
  6246.   return $i if ( $array[$i] eq $default );
  6247.  }
  6248.  return;
  6249. }
  6250.  
  6251. # Dialog for renumber
  6252.  
  6253. sub renumber_dialog {
  6254.  if ( defined $windowrn ) {
  6255.   $windowrn->present;
  6256.   return;
  6257.  }
  6258.  
  6259.  $windowrn = Gscan2pdf::Dialog::Renumber->new(
  6260.   'transient-for'  => $window,
  6261.   document         => $slist,
  6262.   'hide-on-delete' => FALSE,
  6263.   border_width     => $border_width
  6264.  );
  6265.  
  6266.  # Update undo/redo buffers
  6267.  $windowrn->signal_connect(
  6268.   'before-renumber' => sub {
  6269.    take_snapshot();
  6270.   }
  6271.  );
  6272.  
  6273.  $windowrn->show_all;
  6274.  return;
  6275. }
  6276.  
  6277. # Helper function to convert an array of indices into an array of Gscan2pdf::Page objects
  6278.  
  6279. sub indices2pages {
  6280.  my @indices = @_;
  6281.  my @pages;
  6282.  for (@indices) {
  6283.   push @pages, $slist->{data}[$_][2];
  6284.  }
  6285.  return @pages;
  6286. }
  6287.  
  6288. # Rotate selected images
  6289.  
  6290. sub rotate {
  6291.  my ( $angle, $pagelist, $callback ) = @_;
  6292.  
  6293.  # Update undo/redo buffers
  6294.  take_snapshot();
  6295.  
  6296.  for my $page ( @{$pagelist} ) {
  6297.   my ( $signal, $pid );
  6298.   $pid = $slist->rotate(
  6299.    angle           => $angle,
  6300.    page            => $page,
  6301.    queued_callback => sub {
  6302.     return update_tpbar(@_);
  6303.    },
  6304.    started_callback => sub {
  6305.     my ( $thread, $process, $completed, $total ) = @_;
  6306.     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6307.     return TRUE if ( defined $signal );
  6308.    },
  6309.    finished_callback => sub {
  6310.     my ( $new_page, $pending ) = @_;
  6311.     $callback->($new_page) if $callback;
  6312.     $thbox->hide_all unless $pending;
  6313.     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6314.    },
  6315.    error_callback => sub {
  6316.     my ($message) = @_;
  6317.     show_message_dialog( $window, 'error', 'close', $message );
  6318.    },
  6319.    display_callback => sub {
  6320.     my ($new_page) = @_;
  6321.     display_image($new_page);
  6322.    },
  6323.   );
  6324.  }
  6325.  return;
  6326. }
  6327.  
  6328. # Analyse selected images
  6329.  
  6330. sub analyse {
  6331.  my ( $select_blank, $select_dark ) = @_;
  6332.  
  6333.  # Update undo/redo buffers
  6334.  take_snapshot();
  6335.  
  6336.  foreach my $i ( 0 .. $#{ $slist->{data} } ) {
  6337.   my $dirty_time   = $slist->{data}[$i][2]{dirty_time};
  6338.   my $analyse_time = $slist->{data}[$i][2]{analyse_time};
  6339.   $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  6340.   $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  6341.   if ( $analyse_time le $dirty_time ) {
  6342.    $logger->info(
  6343. "Updating: $slist->{data}[$i][0] analyse_time: $analyse_time dirty_time: $dirty_time"
  6344.    );
  6345.    my ( $signal, $pid );
  6346.    $pid = $slist->analyse(
  6347.     page            => $slist->{data}[$i][2],
  6348.     queued_callback => sub {
  6349.      return update_tpbar(@_);
  6350.     },
  6351.     started_callback => sub {
  6352.      my ( $thread, $process, $completed, $total ) = @_;
  6353.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6354.      return TRUE if ( defined $signal );
  6355.     },
  6356.     finished_callback => sub {
  6357.      my ( $new_page, $pending ) = @_;
  6358.      $thbox->hide_all unless $pending;
  6359.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6360.      select_blank_pages()                          if $select_blank;
  6361.      select_dark_pages()                           if $select_dark;
  6362.     },
  6363.     error_callback => sub {
  6364.      my ($message) = @_;
  6365.      show_message_dialog( $window, 'error', 'close', $message );
  6366.     },
  6367.    );
  6368.   }
  6369.  }
  6370.  return;
  6371. }
  6372.  
  6373. # Handle right-clicks
  6374.  
  6375. sub handle_clicks {
  6376.  my ( $widget, $event ) = @_;
  6377.  
  6378.  # $SETTING{'RMB'} = ($event->button == 3);
  6379.  #warn "rmb $SETTING{'RMB'}\n";
  6380.  
  6381.  if ( $event->button == 3 ) {
  6382.   if ( $widget->isa('Gtk2::ImageView') ) {    # main image
  6383.    $uimanager->get_widget('/Detail_Popup')
  6384.      ->popup( undef, undef, undef, undef, $event->button, $event->time );
  6385.   }
  6386.   else {                                      # Thumbnail simplelist
  6387.    $uimanager->get_widget('/Thumb_Popup')
  6388.      ->popup( undef, undef, undef, undef, $event->button, $event->time );
  6389.   }
  6390.  
  6391.   # block event propagation
  6392.   return TRUE;
  6393.  }
  6394.  
  6395.  # allow event propagation
  6396.  return FALSE;
  6397. }
  6398.  
  6399. # Display page selector and on apply threshold accordingly
  6400.  
  6401. sub threshold {
  6402.  
  6403.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  6404.  
  6405.  my $windowt = Gscan2pdf::Dialog->new(
  6406.   'transient-for' => $window,
  6407.   title           => $d->get('Threshold'),
  6408.   border_width    => $border_width
  6409.  );
  6410.  my $vbox = $windowt->get('vbox');
  6411.  
  6412.  # Frame for page range
  6413.  add_page_range($vbox);
  6414.  
  6415.  # SpinButton for threshold
  6416.  my $hboxt = Gtk2::HBox->new;
  6417.  $vbox->pack_start( $hboxt, FALSE, TRUE, 0 );
  6418.  my $label = Gtk2::Label->new( $d->get('Threshold') );
  6419.  $hboxt->pack_start( $label, FALSE, TRUE, 0 );
  6420.  my $labelp = Gtk2::Label->new( $d->get('%') );
  6421.  $hboxt->pack_end( $labelp, FALSE, TRUE, 0 );
  6422.  my $spinbutton = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
  6423.  $spinbutton->set_value( $SETTING{'threshold tool'} );
  6424.  $hboxt->pack_end( $spinbutton, FALSE, TRUE, 0 );
  6425.  
  6426.  # HBox for buttons
  6427.  my $hboxb = _hbox_with_apply_cancel_buttons(
  6428.   sub {
  6429.  
  6430.    # Update undo/redo buffers
  6431.    take_snapshot();
  6432.  
  6433.    $SETTING{'threshold tool'} = $spinbutton->get_value;
  6434.  
  6435.    my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  6436.    my $page     = 0;
  6437.    for my $i (@pagelist) {
  6438.     $page++;
  6439.     my ( $signal, $pid );
  6440.     $pid = $slist->threshold(
  6441.      threshold       => $SETTING{'threshold tool'},
  6442.      page            => $slist->{data}[$i][2],
  6443.      queued_callback => sub {
  6444.       return update_tpbar(@_);
  6445.      },
  6446.      started_callback => sub {
  6447.       my ( $thread, $process, $completed, $total ) = @_;
  6448.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6449.       return TRUE if ( defined $signal );
  6450.      },
  6451.      finished_callback => sub {
  6452.       my ( $new_page, $pending ) = @_;
  6453.       $thbox->hide_all unless $pending;
  6454.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6455.      },
  6456.      error_callback => sub {
  6457.       my ($message) = @_;
  6458.       show_message_dialog( $window, 'error', 'close', $message );
  6459.      },
  6460.      display_callback => sub {
  6461.       my ($new_page) = @_;
  6462.       display_image($new_page);
  6463.      },
  6464.     );
  6465.    }
  6466.   },
  6467.   sub { $windowt->destroy }
  6468.  );
  6469.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  6470.  $windowt->show_all;
  6471.  return;
  6472. }
  6473.  
  6474. # Display page selector and on apply negate accordingly
  6475.  
  6476. sub negate {
  6477.  
  6478.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  6479.  
  6480.  my $windowt = Gscan2pdf::Dialog->new(
  6481.   'transient-for' => $window,
  6482.   title           => $d->get('Negate'),
  6483.   border_width    => $border_width
  6484.  );
  6485.  my $vbox = $windowt->get('vbox');
  6486.  
  6487.  # Frame for page range
  6488.  add_page_range($vbox);
  6489.  
  6490.  # HBox for buttons
  6491.  my $hboxb = _hbox_with_apply_cancel_buttons(
  6492.   sub {
  6493.  
  6494.    # Update undo/redo buffers
  6495.    take_snapshot();
  6496.  
  6497.    my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  6498.    for my $i (@pagelist) {
  6499.     my ( $signal, $pid );
  6500.     $pid = $slist->negate(
  6501.      page            => $slist->{data}[$i][2],
  6502.      queued_callback => sub {
  6503.       return update_tpbar(@_);
  6504.      },
  6505.      started_callback => sub {
  6506.       my ( $thread, $process, $completed, $total ) = @_;
  6507.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6508.       return TRUE if ( defined $signal );
  6509.      },
  6510.      finished_callback => sub {
  6511.       my ( $new_page, $pending ) = @_;
  6512.       $thbox->hide_all unless $pending;
  6513.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6514.      },
  6515.      error_callback => sub {
  6516.       my ($message) = @_;
  6517.       show_message_dialog( $window, 'error', 'close', $message );
  6518.      },
  6519.      display_callback => sub {
  6520.       my ($new_page) = @_;
  6521.       display_image($new_page);
  6522.      },
  6523.     );
  6524.    }
  6525.   },
  6526.   sub { $windowt->destroy }
  6527.  );
  6528.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  6529.  $windowt->show_all;
  6530.  return;
  6531. }
  6532.  
  6533. # Display page selector and on apply unsharp accordingly
  6534.  
  6535. sub unsharp {
  6536.  
  6537.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  6538.  
  6539.  my $windowum = Gscan2pdf::Dialog->new(
  6540.   'transient-for' => $window,
  6541.   title           => $d->get('Unsharp mask'),
  6542.   border_width    => $border_width
  6543.  );
  6544.  my $vbox = $windowum->get('vbox');
  6545.  
  6546.  # Frame for page range
  6547.  add_page_range($vbox);
  6548.  
  6549.  # SpinButton for radius
  6550.  my $hbox = Gtk2::HBox->new;
  6551.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6552.  my $label = Gtk2::Label->new( $d->get('Radius') );
  6553.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6554.  $label = Gtk2::Label->new( $d->get('pixels') );
  6555.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6556.  my $spinbuttonr = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
  6557.  $spinbuttonr->set_value( $SETTING{'unsharp radius'} );
  6558.  $tooltips->set_tip(
  6559.   $spinbuttonr,
  6560.   $d->get(
  6561. 'The radius of the Gaussian, in pixels, not counting the center pixel (0 = automatic).'
  6562.   )
  6563.  );
  6564.  $hbox->pack_end( $spinbuttonr, FALSE, TRUE, 0 );
  6565.  
  6566.  # SpinButton for sigma
  6567.  $hbox = Gtk2::HBox->new;
  6568.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6569.  $label = Gtk2::Label->new( $d->get('Sigma') );
  6570.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6571.  $label = Gtk2::Label->new( $d->get('pixels') );
  6572.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6573.  my $spinbuttons = Gtk2::SpinButton->new_with_range( 0, 5, .1 );
  6574.  $spinbuttons->set_value( $SETTING{'unsharp sigma'} );
  6575.  $tooltips->set_tip( $spinbuttons,
  6576.   $d->get('The standard deviation of the Gaussian.') );
  6577.  $hbox->pack_end( $spinbuttons, FALSE, TRUE, 0 );
  6578.  
  6579.  # SpinButton for amount
  6580.  $hbox = Gtk2::HBox->new;
  6581.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6582.  $label = Gtk2::Label->new( $d->get('Amount') );
  6583.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6584.  $label = Gtk2::Label->new( $d->get('%') );
  6585.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6586.  my $spinbuttona = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
  6587.  $spinbuttona->set_value( $SETTING{'unsharp amount'} );
  6588.  $tooltips->set_tip(
  6589.   $spinbuttona,
  6590.   $d->get(
  6591. 'The percentage of the difference between the original and the blur image that is added back into the original.'
  6592.   )
  6593.  );
  6594.  $hbox->pack_end( $spinbuttona, FALSE, TRUE, 0 );
  6595.  
  6596.  # SpinButton for threshold
  6597.  $hbox = Gtk2::HBox->new;
  6598.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6599.  $label = Gtk2::Label->new( $d->get('Threshold') );
  6600.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6601.  my $spinbuttont = Gtk2::SpinButton->new_with_range( 0, 1, 0.01 );
  6602.  $spinbuttont->set_value( $SETTING{'unsharp threshold'} );
  6603.  $tooltips->set_tip(
  6604.   $spinbuttont,
  6605.   $d->get(
  6606. 'The threshold, as a fraction of QuantumRange, needed to apply the difference amount.'
  6607.   )
  6608.  );
  6609.  $hbox->pack_end( $spinbuttont, FALSE, TRUE, 0 );
  6610.  
  6611.  # HBox for buttons
  6612.  my $hboxb = _hbox_with_apply_cancel_buttons(
  6613.   sub {
  6614.  
  6615.    # Update undo/redo buffers
  6616.    take_snapshot();
  6617.  
  6618.    $SETTING{'unsharp radius'}    = $spinbuttonr->get_value;
  6619.    $SETTING{'unsharp sigma'}     = $spinbuttons->get_value;
  6620.    $SETTING{'unsharp amount'}    = $spinbuttona->get_value;
  6621.    $SETTING{'unsharp threshold'} = $spinbuttont->get_value;
  6622.  
  6623.    my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  6624.    for my $i (@pagelist) {
  6625.     my ( $signal, $pid );
  6626.     $pid = $slist->unsharp(
  6627.      page            => $slist->{data}[$i][2],
  6628.      radius          => $SETTING{'unsharp radius'},
  6629.      sigma           => $SETTING{'unsharp sigma'},
  6630.      amount          => $SETTING{'unsharp amount'},
  6631.      threshold       => $SETTING{'unsharp threshold'},
  6632.      queued_callback => sub {
  6633.       return update_tpbar(@_);
  6634.      },
  6635.      started_callback => sub {
  6636.       my ( $thread, $process, $completed, $total ) = @_;
  6637.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6638.       return TRUE if ( defined $signal );
  6639.      },
  6640.      finished_callback => sub {
  6641.       my ( $new_page, $pending ) = @_;
  6642.       $thbox->hide_all unless $pending;
  6643.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6644.      },
  6645.      error_callback => sub {
  6646.       my ($message) = @_;
  6647.       show_message_dialog( $window, 'error', 'close', $message );
  6648.      },
  6649.      display_callback => sub {
  6650.       my ($new_page) = @_;
  6651.       display_image($new_page);
  6652.      },
  6653.     );
  6654.    }
  6655.   },
  6656.   sub { $windowum->destroy }
  6657.  );
  6658.  $windowum->show_all;
  6659.  return;
  6660. }
  6661.  
  6662. # Callback for change Gtk2::ImageView::Tool
  6663.  
  6664. sub change_image_tool_cb {
  6665.  my ( $action, $current ) = @_;
  6666.  my $value = $current->get_current_value();
  6667.  my $tool  = $selector;
  6668.  if ( $value == 10 ) {
  6669.   $tool = $dragger;
  6670.  }
  6671.  elsif ( $value == 30 ) {
  6672.   $tool = $painter;
  6673.  }
  6674.  $view->set_tool($tool);
  6675.  if ( $value == 20
  6676.   and defined( $SETTING{selection} )
  6677.   and @{ $SETTING{selection} } == 4 )
  6678.  {
  6679.   $selector->signal_handler_block( $selector->{selection_changed_signal} );
  6680.   $selector->set_selection(
  6681.    Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
  6682.   $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
  6683.  }
  6684.  return;
  6685. }
  6686.  
  6687. # Display page selector and on apply crop accordingly
  6688.  
  6689. sub crop {
  6690.  
  6691.  if ( defined $windowc ) {
  6692.   $windowc->present;
  6693.   return;
  6694.  }
  6695.  
  6696.  # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
  6697.  
  6698.  $windowc = Gscan2pdf::Dialog->new(
  6699.   'transient-for'  => $window,
  6700.   title            => $d->get('Crop'),
  6701.   'hide-on-delete' => TRUE,
  6702.   border_width     => $border_width
  6703.  );
  6704.  my $vbox = $windowc->get('vbox');
  6705.  
  6706.  # Frame for page range
  6707.  add_page_range($vbox);
  6708.  
  6709.  # SpinButton for x
  6710.  my $hbox = Gtk2::HBox->new;
  6711.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6712.  my $label = Gtk2::Label->new( $d->get('x') );
  6713.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6714.  $label = Gtk2::Label->new( $d->get('pixels') );
  6715.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6716.  $sb_selector_x = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
  6717.  $tooltips->set_tip( $sb_selector_x,
  6718.   $d->get('The x-position of the left hand edge of the crop.') );
  6719.  $hbox->pack_end( $sb_selector_x, FALSE, TRUE, 0 );
  6720.  
  6721.  # SpinButton for y
  6722.  $hbox = Gtk2::HBox->new;
  6723.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6724.  $label = Gtk2::Label->new( $d->get('y') );
  6725.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6726.  $label = Gtk2::Label->new( $d->get('pixels') );
  6727.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6728.  $sb_selector_y = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
  6729.  $tooltips->set_tip( $sb_selector_y,
  6730.   $d->get('The y-position of the top edge of the crop.') );
  6731.  $hbox->pack_end( $sb_selector_y, FALSE, TRUE, 0 );
  6732.  
  6733.  # SpinButton for w
  6734.  $hbox = Gtk2::HBox->new;
  6735.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6736.  $label = Gtk2::Label->new( $d->get('Width') );
  6737.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6738.  $label = Gtk2::Label->new( $d->get('pixels') );
  6739.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6740.  $sb_selector_w = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
  6741.  $tooltips->set_tip( $sb_selector_w, $d->get('The width of the crop.') );
  6742.  $hbox->pack_end( $sb_selector_w, FALSE, TRUE, 0 );
  6743.  
  6744.  # SpinButton for h
  6745.  $hbox = Gtk2::HBox->new;
  6746.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  6747.  $label = Gtk2::Label->new( $d->get('Height') );
  6748.  $hbox->pack_start( $label, FALSE, TRUE, 0 );
  6749.  $label = Gtk2::Label->new( $d->get('pixels') );
  6750.  $hbox->pack_end( $label, FALSE, TRUE, 0 );
  6751.  $sb_selector_h = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
  6752.  $tooltips->set_tip( $sb_selector_h, $d->get('The height of the crop.') );
  6753.  $hbox->pack_end( $sb_selector_h, FALSE, TRUE, 0 );
  6754.  
  6755.  # Callbacks if the spinbuttons change
  6756.  $sb_selector_x->signal_connect(
  6757.   'value-changed' => sub {
  6758.    $SETTING{selection}[0] = $sb_selector_x->get_value;
  6759.    $sb_selector_w->set_range( 0, $current_page->{w} - $SETTING{selection}[0] );
  6760.    update_selector();
  6761.   }
  6762.  );
  6763.  $sb_selector_y->signal_connect(
  6764.   'value-changed' => sub {
  6765.    $SETTING{selection}[1] = $sb_selector_y->get_value;
  6766.    $sb_selector_h->set_range( 0, $current_page->{h} - $SETTING{selection}[1] );
  6767.    update_selector();
  6768.   }
  6769.  );
  6770.  $sb_selector_w->signal_connect(
  6771.   'value-changed' => sub {
  6772.    $SETTING{selection}[2] = $sb_selector_w->get_value;
  6773.    $sb_selector_x->set_range( 0, $current_page->{w} - $SETTING{selection}[2] );
  6774.    update_selector();
  6775.   }
  6776.  );
  6777.  $sb_selector_h->signal_connect(
  6778.   'value-changed' => sub {
  6779.    $SETTING{selection}[3] = $sb_selector_h->get_value;
  6780.    $sb_selector_y->set_range( 0, $current_page->{h} - $SETTING{selection}[3] );
  6781.    update_selector();
  6782.   }
  6783.  );
  6784.  
  6785.  $sb_selector_x->set_value( $SETTING{selection}[0] )
  6786.    if ( defined $SETTING{selection}[0] );
  6787.  $sb_selector_y->set_value( $SETTING{selection}[1] )
  6788.    if ( defined $SETTING{selection}[1] );
  6789.  $sb_selector_w->set_value( $SETTING{selection}[2] )
  6790.    if ( defined $SETTING{selection}[2] );
  6791.  $sb_selector_h->set_value( $SETTING{selection}[3] )
  6792.    if ( defined $SETTING{selection}[3] );
  6793.  
  6794.  # HBox for buttons
  6795.  my $hboxb = _hbox_with_apply_cancel_buttons(
  6796.   sub {
  6797.  
  6798.    return unless ( @{ $SETTING{selection} } );
  6799.  
  6800.    # Update undo/redo buffers
  6801.    take_snapshot();
  6802.  
  6803.    my @pagelist = $slist->get_page_index( $SETTING{'Page range'} );
  6804.    for my $i (@pagelist) {
  6805.     my ( $signal, $pid );
  6806.     $pid = $slist->crop(
  6807.      page            => $slist->{data}[$i][2],
  6808.      x               => $SETTING{selection}[0],
  6809.      y               => $SETTING{selection}[1],
  6810.      w               => $SETTING{selection}[2],
  6811.      h               => $SETTING{selection}[3],
  6812.      queued_callback => sub {
  6813.       return update_tpbar(@_);
  6814.      },
  6815.      started_callback => sub {
  6816.       my ( $thread, $process, $completed, $total ) = @_;
  6817.       $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6818.       return TRUE if ( defined $signal );
  6819.      },
  6820.      finished_callback => sub {
  6821.       my ( $new_page, $pending ) = @_;
  6822.       $thbox->hide_all unless $pending;
  6823.       $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6824.      },
  6825.      error_callback => sub {
  6826.       my ($message) = @_;
  6827.       show_message_dialog( $window, 'error', 'close', $message );
  6828.      },
  6829.      display_callback => sub {
  6830.       my ($new_page) = @_;
  6831.       display_image($new_page);
  6832.      },
  6833.     );
  6834.    }
  6835.  
  6836.   },
  6837.   sub { $windowc->hide }
  6838.  );
  6839.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  6840.  $windowc->show_all;
  6841.  return;
  6842. }
  6843.  
  6844. sub update_selector {
  6845.  my $sel = $selector->get_selection;
  6846.  $selector->signal_handler_block( $selector->{selection_changed_signal} );
  6847.  $selector->set_selection(
  6848.   Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) )
  6849.    if ( defined $sel );
  6850.  $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
  6851.  return;
  6852. }
  6853.  
  6854. # Run a user-defined tool on the selected images
  6855.  
  6856. sub user_defined_tool {
  6857.  my ($action) = @_;
  6858.  my @pages = $slist->get_selected_indices;
  6859.  
  6860.  # Update undo/redo buffers
  6861.  take_snapshot();
  6862.  
  6863.  my $cmd = $action->get('label');
  6864.  for my $i (@pages) {
  6865.   my ( $signal, $pid );
  6866.   $pid = $slist->user_defined(
  6867.    page            => $slist->{data}[$i][2],
  6868.    command         => $cmd,
  6869.    queued_callback => sub {
  6870.     return update_tpbar(@_);
  6871.    },
  6872.    started_callback => sub {
  6873.     my ( $thread, $process, $completed, $total ) = @_;
  6874.     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6875.     return TRUE if ( defined $signal );
  6876.    },
  6877.    finished_callback => sub {
  6878.     my ( $new_page, $pending ) = @_;
  6879.     $thbox->hide_all unless $pending;
  6880.     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6881.    },
  6882.    error_callback => sub {
  6883.     my ($message) = @_;
  6884.     show_message_dialog( $window, 'error', 'close', $message );
  6885.    },
  6886.    display_callback => sub {
  6887.     my ($new_page) = @_;
  6888.     display_image($new_page);
  6889.    },
  6890.   );
  6891.  }
  6892.  return;
  6893. }
  6894.  
  6895. # guess from which window the sub was called
  6896.  
  6897. sub get_parent {
  6898.  my ( $w1, $w2, $w3 ) = @_;
  6899.  if ( defined($w1) and $w1->visible ) {
  6900.   return $w1;
  6901.  }
  6902.  elsif ( defined($w2) and $w2->visible ) {
  6903.   return $w2;
  6904.  }
  6905.  else {
  6906.   return $w3;
  6907.  }
  6908.  return;
  6909. }
  6910.  
  6911. # queue $page to be processed by unpaper
  6912.  
  6913. sub unpaper_page {
  6914.  my ( $pages, $options, $callback ) = @_;
  6915.  $options = '' unless ( defined $options );
  6916.  
  6917.  # Update undo/redo buffers
  6918.  take_snapshot();
  6919.  
  6920.  for my $pageobject ( @{$pages} ) {
  6921.   my ( $signal, $pid );
  6922.   $pid = $slist->unpaper(
  6923.    page            => $pageobject,
  6924.    options         => $options,
  6925.    queued_callback => sub {
  6926.     return update_tpbar(@_);
  6927.    },
  6928.    started_callback => sub {
  6929.     my ( $thread, $process, $completed, $total ) = @_;
  6930.     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  6931.     return TRUE if ( defined $signal );
  6932.    },
  6933.    finished_callback => sub {
  6934.     my ( $new_page, $pending ) = @_;
  6935.     $callback->($new_page) if $callback;
  6936.     $thbox->hide_all unless $pending;
  6937.     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  6938.    },
  6939.    error_callback => sub {
  6940.     my ($message) = @_;
  6941.     show_message_dialog( $window, 'error', 'close', $message );
  6942.    },
  6943.    display_callback => sub {
  6944.     my ($new_page) = @_;
  6945.     display_image($new_page);
  6946.    },
  6947.   );
  6948.  }
  6949.  
  6950.  return;
  6951. }
  6952.  
  6953. # Run unpaper to clean up scan.
  6954.  
  6955. sub unpaper {
  6956.  
  6957.  if ( defined $windowu ) {
  6958.   $windowu->present;
  6959.   return;
  6960.  }
  6961.  
  6962.  $windowu = Gscan2pdf::Dialog->new(
  6963.   'transient-for'  => $window,
  6964.   title            => $d->get('unpaper'),
  6965.   'hide-on-delete' => TRUE,
  6966.   border_width     => $border_width
  6967.  );
  6968.  my $vbox = $windowu->get('vbox');
  6969.  $unpaper->add_options($vbox);
  6970.  
  6971.  # Frame for page range
  6972.  add_page_range($vbox);
  6973.  
  6974.  # HBox for buttons
  6975.  my $hboxb = Gtk2::HBox->new;
  6976.  $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
  6977.  
  6978.  # OK button
  6979.  my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
  6980.  $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
  6981.  $sbutton->signal_connect(
  6982.   clicked => sub {
  6983.  
  6984.    # Update undo/redo buffers
  6985.    take_snapshot();
  6986.  
  6987.    # Update $SETTING
  6988.    $SETTING{'unpaper options'} = $unpaper->get_options;
  6989.  
  6990.    # run unpaper
  6991.    my @pages =
  6992.      indices2pages( $slist->get_page_index( $SETTING{'Page range'} ) );
  6993.    unpaper_page( \@pages, $unpaper->get_cmdline );
  6994.  
  6995.    $windowu->hide;
  6996.   }
  6997.  );
  6998.  
  6999.  # Cancel button
  7000.  my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
  7001.  $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
  7002.  $cbutton->signal_connect( clicked => sub { $windowu->hide; } );
  7003.  
  7004.  $windowu->show_all;
  7005.  return;
  7006. }
  7007.  
  7008. # Add $page to the OCR stack, setting it off if not running.
  7009.  
  7010. sub ocr_page {
  7011.  my ($pages) = @_;
  7012.  
  7013.  for my $page ( @{$pages} ) {
  7014.   my ( $signal, $pid );
  7015.   if ( $SETTING{'ocr engine'} eq 'gocr' ) {
  7016.    $pid = $slist->gocr(
  7017.     page            => $page,
  7018.     queued_callback => sub {
  7019.      return update_tpbar(@_);
  7020.     },
  7021.     started_callback => sub {
  7022.      my ( $thread, $process, $completed, $total ) = @_;
  7023.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  7024.      return TRUE if ( defined $signal );
  7025.     },
  7026.     finished_callback => sub {
  7027.      my ( $new_page, $pending ) = @_;
  7028.      $thbox->hide_all unless $pending;
  7029.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  7030.     },
  7031.     error_callback => sub {
  7032.      my ($message) = @_;
  7033.      show_message_dialog( $window, 'error', 'close', $message );
  7034.     },
  7035.     display_callback => sub {
  7036.      my ($new_page) = @_;
  7037.      my @page = $slist->get_selected_indices;
  7038.      create_canvas($new_page)
  7039.        if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
  7040.     }
  7041.    );
  7042.   }
  7043.   elsif ( $SETTING{'ocr engine'} eq 'tesseract' ) {
  7044.    $pid = $slist->tesseract(
  7045.     page            => $page,
  7046.     language        => $SETTING{'ocr language'},
  7047.     queued_callback => sub {
  7048.      return update_tpbar(@_);
  7049.     },
  7050.     started_callback => sub {
  7051.      my ( $thread, $process, $completed, $total ) = @_;
  7052.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  7053.      return TRUE if ( defined $signal );
  7054.     },
  7055.     finished_callback => sub {
  7056.      my ( $new_page, $pending ) = @_;
  7057.      $thbox->hide_all unless $pending;
  7058.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  7059.     },
  7060.     error_callback => sub {
  7061.      my ($message) = @_;
  7062.      show_message_dialog( $window, 'error', 'close', $message );
  7063.     },
  7064.     display_callback => sub {
  7065.      my ($new_page) = @_;
  7066.      my @page = $slist->get_selected_indices;
  7067.      create_canvas($new_page)
  7068.        if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
  7069.     }
  7070.    );
  7071.   }
  7072.   elsif ( $SETTING{'ocr engine'} eq 'ocropus' ) {
  7073.    $pid = $slist->ocropus(
  7074.     page            => $page,
  7075.     language        => $SETTING{'ocr language'},
  7076.     queued_callback => sub {
  7077.      return update_tpbar(@_);
  7078.     },
  7079.     started_callback => sub {
  7080.      my ( $thread, $process, $completed, $total ) = @_;
  7081.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  7082.      return TRUE if ( defined $signal );
  7083.     },
  7084.     finished_callback => sub {
  7085.      my ( $new_page, $pending ) = @_;
  7086.      $thbox->hide_all unless $pending;
  7087.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  7088.     },
  7089.     error_callback => sub {
  7090.      my ($message) = @_;
  7091.      show_message_dialog( $window, 'error', 'close', $message );
  7092.     },
  7093.     display_callback => sub {
  7094.      my ($new_page) = @_;
  7095.      my @page = $slist->get_selected_indices;
  7096.      create_canvas($new_page)
  7097.        if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
  7098.     }
  7099.    );
  7100.   }
  7101.   else {    # cuneiform
  7102.    $pid = $slist->cuneiform(
  7103.     page            => $page,
  7104.     language        => $SETTING{'ocr language'},
  7105.     queued_callback => sub {
  7106.      return update_tpbar(@_);
  7107.     },
  7108.     started_callback => sub {
  7109.      my ( $thread, $process, $completed, $total ) = @_;
  7110.      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  7111.      return TRUE if ( defined $signal );
  7112.     },
  7113.     finished_callback => sub {
  7114.      my ( $new_page, $pending ) = @_;
  7115.      $thbox->hide_all unless $pending;
  7116.      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  7117.     },
  7118.     error_callback => sub {
  7119.      my ($message) = @_;
  7120.      show_message_dialog( $window, 'error', 'close', $message );
  7121.     },
  7122.     display_callback => sub {
  7123.      my ($new_page) = @_;
  7124.      my @page = $slist->get_selected_indices;
  7125.      create_canvas($new_page)
  7126.        if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
  7127.     }
  7128.    );
  7129.   }
  7130.  }
  7131.  return;
  7132. }
  7133.  
  7134. # Create a combobox from an array and set the default
  7135.  
  7136. sub combobox_from_array {
  7137.  my (@array) = @_;
  7138.  
  7139.  # Fill ComboBox
  7140.  my $combobox = Gtk2::ComboBox->new_text;
  7141.  foreach (@array) {
  7142.   $combobox->append_text( $_->[1] );
  7143.  }
  7144.  return $combobox;
  7145. }
  7146.  
  7147. # Create a combobox from an array and set the default
  7148.  
  7149. sub combobox_set_active {
  7150.  my ( $combobox, $default, @array ) = @_;
  7151.  
  7152.  # Fill ComboBox
  7153.  my $i = 0;
  7154.  my $o;
  7155.  if ( defined($default) ) {
  7156.   foreach (@array) {
  7157.    $o = $i if ( defined( $_->[0] ) and $_->[0] eq $default );
  7158.    ++$i;
  7159.   }
  7160.  }
  7161.  $o = 0 unless ( defined $o );
  7162.  $combobox->set_active($o);
  7163.  return;
  7164. }
  7165.  
  7166. # Add hbox for tesseract languages
  7167.  
  7168. sub add_tess_languages {
  7169.  my ($vbox) = @_;
  7170.  
  7171.  my $hbox = Gtk2::HBox->new;
  7172.  $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
  7173.  my $label = Gtk2::Label->new( $d->get('Language to recognise') );
  7174.  $hbox->pack_start( $label, TRUE, TRUE, 0 );
  7175.  
  7176.  # Tesseract language files
  7177.  my @tesslang;
  7178.  for ( sort { $a cmp $b } keys %{ Gscan2pdf::Tesseract->languages } ) {
  7179.   push @tesslang, [ $_, $d->get( ${ Gscan2pdf::Tesseract->languages }{$_} ) ];
  7180.  }
  7181.  
  7182.  # If there are no language files, then we have tesseract-1.0, i.e. English
  7183.  unless (@tesslang) {
  7184.   push @tesslang, [ undef, $d->get('English') ];
  7185.   $logger->info("No tesseract languages found");
  7186.  }
  7187.  
  7188.  my $combobox = combobox_from_array(@tesslang);
  7189.  combobox_set_active( $combobox, $SETTING{'ocr language'}, @tesslang );
  7190.  $hbox->pack_end( $combobox, TRUE, TRUE, 0 );
  7191.  return $hbox, $combobox, @tesslang;
  7192. }
  7193.  
  7194. # Add hbox for cuneiform languages
  7195.  
  7196. sub add_cf_languages {
  7197.  my ($vbox) = @_;
  7198.  
  7199.  my $hbox = Gtk2::HBox->new;
  7200.  $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
  7201.  my $label = Gtk2::Label->new( $d->get('Language to recognise') );
  7202.  $hbox->pack_start( $label, TRUE, TRUE, 0 );
  7203.  
  7204.  # Tesseract language files
  7205.  my @lang;
  7206.  for ( sort { $a cmp $b } keys %{ Gscan2pdf::Cuneiform->languages } ) {
  7207.   push @lang, [ $_, $d->get( ${ Gscan2pdf::Cuneiform->languages }{$_} ) ];
  7208.  }
  7209.  
  7210.  my $combobox = combobox_from_array(@lang);
  7211.  combobox_set_active( $combobox, $SETTING{'ocr language'}, @lang );
  7212.  $hbox->pack_end( $combobox, TRUE, TRUE, 0 );
  7213.  return $hbox, $combobox, @lang;
  7214. }
  7215.  
  7216. # Run OCR on current page and display result
  7217.  
  7218. sub OCR {
  7219.  
  7220.  if ( defined $windowo ) {
  7221.   $windowo->present;
  7222.   return;
  7223.  }
  7224.  
  7225.  $windowo = Gscan2pdf::Dialog->new(
  7226.   'transient-for'  => $window,
  7227.   title            => $d->get('OCR'),
  7228.   'hide-on-delete' => TRUE,
  7229.   border_width     => $border_width
  7230.  );
  7231.  my $vbox = $windowo->get('vbox');
  7232.  
  7233.  # OCR engine selection
  7234.  my $hboxe = Gtk2::HBox->new;
  7235.  $vbox->pack_start( $hboxe, TRUE, TRUE, 0 );
  7236.  my $label = Gtk2::Label->new( $d->get('OCR Engine') );
  7237.  $hboxe->pack_start( $label, FALSE, FALSE, 0 );
  7238.  my $combobe = combobox_from_array(@ocr_engine);
  7239.  combobox_set_active( $combobe, $SETTING{'ocr engine'}, @ocr_engine );
  7240.  $hboxe->pack_end( $combobe, FALSE, FALSE, 0 );
  7241.  my ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );
  7242.  
  7243.  if ( $dependencies{tesseract} ) {
  7244.   ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vbox);
  7245.   $combobe->signal_connect(
  7246.    changed => sub {
  7247.     if ($ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
  7248.      or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
  7249.     {
  7250.      $hboxtl->show_all;
  7251.     }
  7252.     else {
  7253.      $hboxtl->hide_all;
  7254.     }
  7255.    }
  7256.   );
  7257.  }
  7258.  if ( $dependencies{cuneiform} ) {
  7259.   ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vbox);
  7260.   $combobe->signal_connect(
  7261.    changed => sub {
  7262.     if ( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) {
  7263.      $hboxcl->show_all;
  7264.     }
  7265.     else {
  7266.      $hboxcl->hide_all;
  7267.     }
  7268.    }
  7269.   );
  7270.  }
  7271.  
  7272.  # Frame for page range
  7273.  add_page_range($vbox);
  7274.  
  7275.  # HBox for buttons
  7276.  my $hbox = Gtk2::HBox->new;
  7277.  $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
  7278.  
  7279.  # Start button
  7280.  my $obutton = Gtk2::Button->new( $d->get('Start OCR') );
  7281.  $hbox->pack_start( $obutton, TRUE, TRUE, 0 );
  7282.  $obutton->signal_connect(
  7283.   clicked => sub {
  7284.    $SETTING{'ocr engine'}   = $ocr_engine[ $combobe->get_active ]->[0];
  7285.    $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
  7286.      if ( $SETTING{'ocr engine'} eq 'tesseract'
  7287.     or $SETTING{'ocr engine'} eq 'ocropus' );
  7288.    $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
  7289.      if ( $SETTING{'ocr engine'} eq 'cuneiform' );
  7290.  
  7291.    # fill $pagelist with filenames depending on which radiobutton is active
  7292.    my @pages =
  7293.      indices2pages( $slist->get_page_index( $SETTING{'Page range'} ) );
  7294.    if ( not @pages ) {
  7295.     show_message_dialog( $windowo, 'error', 'close',
  7296.      $d->get('No page selected') );
  7297.     return;
  7298.    }
  7299.    ocr_page( \@pages );
  7300.    $windowo->hide;
  7301.   }
  7302.  );
  7303.  
  7304.  # Close button
  7305.  my $cbutton = Gtk2::Button->new_from_stock('gtk-close');
  7306.  $hbox->pack_end( $cbutton, FALSE, FALSE, 0 );
  7307.  $cbutton->signal_connect( clicked => sub { $windowo->hide; } );
  7308.  
  7309.  $windowo->show_all;
  7310.  $hboxtl->hide_all
  7311.    if (
  7312.   defined($hboxtl)
  7313.   and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
  7314.    or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
  7315.    );
  7316.  $hboxcl->hide_all
  7317.    if ( defined($hboxcl)
  7318.   and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) );
  7319.  return;
  7320. }
  7321.  
  7322. # Remove temporary files, note window state, save settings and quit.
  7323.  
  7324. sub quit {
  7325.  
  7326.  # Check that all pages have been saved
  7327.  for ( my $i = 0 ; $i < @{ $slist->{data} } ; $i++ ) {
  7328.   if ( not $slist->{data}[$i][2]{saved} ) {
  7329.    my $response =
  7330.      show_message_dialog( $window, 'question', 'ok-cancel',
  7331.     $d->get("Some pages have not been saved.\nDo you really want to quit?") );
  7332.    if ( $response ne 'ok' ) {
  7333.     return FALSE;
  7334.    }
  7335.    else {
  7336.     last;
  7337.    }
  7338.   }
  7339.  }
  7340.  
  7341.  # Make sure that we are back in the start directory, otherwise we can't delete
  7342.  # the temp dir.
  7343.  chdir $SETTING{'cwd'};
  7344.  
  7345.  # Remove temporary files (for some reason File::Temp wasn't doing its job here)
  7346.  unlink <$session/*>;
  7347.  rmdir $session;
  7348.  
  7349.  # Write window state to settings
  7350.  ( $SETTING{'window_width'}, $SETTING{'window_height'} ) = $window->get_size;
  7351.  ( $SETTING{'window_x'}, $SETTING{'window_y'} ) = $window->get_position;
  7352.  $SETTING{'thumb panel'} = $hpaned->get_position;
  7353.  
  7354.  # delete $SETTING{'RMB'};
  7355.  
  7356.  # Write config file
  7357.  $conf->save_file( $rc, \%SETTING );
  7358.  $logger->info("Wrote config to $ENV{'HOME'}/.$prog_name");
  7359.  
  7360.  kill_threads();
  7361.  kill_subs();
  7362.  $logger->debug("Quitting");
  7363.  return TRUE;
  7364. }
  7365.  
  7366. # kill all threads
  7367.  
  7368. sub kill_threads {
  7369.  Gscan2pdf::Frontend::Sane->quit();
  7370.  Gscan2pdf::Document->quit();
  7371.  return;
  7372. }
  7373.  
  7374. # View POD
  7375.  
  7376. sub view_pod {
  7377.  
  7378.  if ( defined $windowh ) {
  7379.   $windowh->present;
  7380.   return;
  7381.  }
  7382.  
  7383.  try { require Gtk2::Ex::PodViewer }
  7384.  catch {
  7385.   show_message_dialog(
  7386.    $window, 'error', 'close',
  7387.    sprintf(
  7388.     $d->get(
  7389.          "The help viewer requires module Gtk2::Ex::PodViewer\n"
  7390.        . "Alternatively, try: %s %s\n\n"
  7391.     ),
  7392.     $prog_name,
  7393.     "--help"
  7394.    )
  7395.   );
  7396.   return;
  7397.  };
  7398.  
  7399.  # Window
  7400.  $windowh = Gtk2::Window->new;
  7401.  $windowh->set_transient_for($window);    # Assigns parent
  7402.  $windowh->signal_connect(
  7403.   delete_event => sub {
  7404.    $windowh->hide;
  7405.    return TRUE;    # ensures that the window is not destroyed
  7406.   }
  7407.  );
  7408.  $windowh->set_default_size( 800, 600 );
  7409.  
  7410.  # Vertical divider between index and viewer
  7411.  my $pane = Gtk2::HPaned->new;
  7412.  $pane->set_position(200);
  7413.  $windowh->add($pane);
  7414.  
  7415.  # Index list
  7416.  my $index = Gtk2::Ex::Simple::List->new(
  7417.   'icon'  => 'pixbuf',
  7418.   'title' => 'text',
  7419.   'link'  => 'hstring'
  7420.  );
  7421.  $index->set_headers_visible(FALSE);
  7422.  $index->get_column(1)->set_sizing('autosize');
  7423.  
  7424.  # Index
  7425.  my $index_scrwin = Gtk2::ScrolledWindow->new;
  7426.  $index_scrwin->set_shadow_type('in');
  7427.  $index_scrwin->set_policy( 'automatic', 'automatic' );
  7428.  $index_scrwin->add_with_viewport($index);
  7429.  $index_scrwin->get_child->set_shadow_type('none');
  7430.  
  7431.  # Viewer
  7432.  my $viewer = Gtk2::Ex::PodViewer->new;
  7433.  $viewer->set_border_width($border_width);
  7434.  $viewer->set_cursor_visible(FALSE);
  7435.  $index->get_selection->signal_connect(
  7436.   'changed',
  7437.   sub {
  7438.    my $idx  = ( $index->get_selected_indices )[0];
  7439.    my $mark = $index->{data}[$idx][2];
  7440.    $viewer->jump_to($mark);
  7441.    return TRUE;
  7442.   }
  7443.  );
  7444.  
  7445.  my $viewer_scrwin = Gtk2::ScrolledWindow->new;
  7446.  $viewer_scrwin->set_shadow_type('in');
  7447.  $viewer_scrwin->set_policy( 'automatic', 'automatic' );
  7448.  $viewer_scrwin->add($viewer);
  7449.  
  7450.  $pane->add1($index_scrwin);
  7451.  $pane->add2($viewer_scrwin);
  7452.  
  7453.  $viewer->load($0);
  7454.  
  7455.  # Index contents
  7456.  my $idx_pbf = Gtk2::Image->new->render_icon( 'gtk-jump-to', 'menu' );
  7457.  map { push( @{ $index->{data} }, [ $idx_pbf, strippod($_), $_ ] ) }
  7458.    $viewer->get_marks;
  7459.  
  7460.  $windowh->show_all;
  7461.  return;
  7462. }
  7463.  
  7464. # Remove formatting characters
  7465.  
  7466. sub strippod {
  7467.  my $text = shift;
  7468.  $text =~ s/B<([^<]*)>/$1/gx;
  7469.  $text =~ s/E<gt>/>/gx;
  7470.  return $text;
  7471. }
  7472.  
  7473. # Add option, value pair to options
  7474.  
  7475. sub add_to_options {
  7476.  my ( $option, $value ) = @_;
  7477.  
  7478.  # Dig out of possible options, if defined
  7479.  if ( defined( $pddo{$option} )
  7480.   and defined( $pddo{$option}{values} )
  7481.   and defined( $pddo{$option}{values}{$value} ) )
  7482.  {
  7483.   $ddo{$option}{values}{$value} = $pddo{$option}{values}{$value};
  7484.  }
  7485.  else {
  7486.   $ddo{$option}{values}{$value} = $value;
  7487.  }
  7488.  return;
  7489. }
  7490.  
  7491. # Get option string from label
  7492.  
  7493. sub get_key {
  7494.  my ( $options, $value ) = @_;
  7495.  foreach ( keys %$options ) {
  7496.   return $_ if ( $options->{$_}{string} eq $value );
  7497.  }
  7498.  return;
  7499. }
  7500.  
  7501. # Get value string from combobox
  7502.  
  7503. sub get_value {
  7504.  my ( $options, $option, $value ) = @_;
  7505.  foreach ( keys %{ $options->{$option}{values} } ) {
  7506.   return $_ if ( $options->{$option}{values}{$_} eq $value );
  7507.  }
  7508.  return;
  7509. }
  7510.  
  7511. # Update undo/redo buffers before doing something
  7512.  
  7513. sub take_snapshot {
  7514.  
  7515.  # Deep copy the tied data. Otherwise, very bad things happen.
  7516.  @undo_buffer = map { [@$_] } @{ $slist->{data} };
  7517.  @undo_selection = $slist->get_selected_indices;
  7518.  $logger->debug( Dumper( \@undo_buffer ) );
  7519.  
  7520.  # Unghost Undo/redo
  7521.  $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(TRUE);
  7522.  $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(TRUE);
  7523.  
  7524.  # Save session
  7525.  $slist->save_session($session);
  7526.  return;
  7527. }
  7528.  
  7529. # Put things back to last snapshot after updating redo buffer
  7530.  
  7531. sub undo {
  7532.  $logger->info("Undoing");
  7533.  
  7534.  # Deep copy the tied data. Otherwise, very bad things happen.
  7535.  @redo_buffer = map { [@$_] } @{ $slist->{data} };
  7536.  @redo_selection = $slist->get_selected_indices;
  7537.  $logger->debug("redo_selection, undo_selection:");
  7538.  $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
  7539.  $logger->debug("redo_buffer, undo_buffer:");
  7540.  $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );
  7541.  
  7542.  # Block slist signals whilst updating
  7543.  $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  7544.  @{ $slist->{data} } = @undo_buffer;
  7545.  
  7546.  # Unblock slist signals now finished
  7547.  $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  7548.  
  7549.  # Reselect the pages to display the detail view
  7550.  $slist->select(@undo_selection);
  7551.  
  7552.  # Update menus/buttons
  7553.  update_uimanager();
  7554.  $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
  7555.  $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(TRUE);
  7556.  $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);
  7557.  $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(TRUE);
  7558.  return;
  7559. }
  7560.  
  7561. # Put things back to last snapshot after updating redo buffer
  7562.  
  7563. sub unundo {
  7564.  $logger->info("Redoing");
  7565.  
  7566.  # Deep copy the tied data. Otherwise, very bad things happen.
  7567.  @undo_buffer = map { [@$_] } @{ $slist->{data} };
  7568.  @undo_selection = $slist->get_selected_indices;
  7569.  $logger->debug("redo_selection, undo_selection:");
  7570.  $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
  7571.  $logger->debug("redo_buffer, undo_buffer:");
  7572.  $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );
  7573.  
  7574.  # Block slist signals whilst updating
  7575.  $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  7576.  @{ $slist->{data} } = @redo_buffer;
  7577.  
  7578.  # Unblock slist signals now finished
  7579.  $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  7580.  
  7581.  # Reselect the pages to display the detail view
  7582.  $slist->select(@redo_selection);
  7583.  
  7584.  # Update menus/buttons
  7585.  update_uimanager();
  7586.  $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(TRUE);
  7587.  $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(FALSE);
  7588.  $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(TRUE);
  7589.  $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(FALSE);
  7590.  return;
  7591. }
  7592.  
  7593. # Initialise IconFactory
  7594.  
  7595. sub init_icons {
  7596.  my @icons = @_;
  7597.  return if defined $IconFactory;
  7598.  
  7599.  $IconFactory = Gtk2::IconFactory->new();
  7600.  $IconFactory->add_default();
  7601.  
  7602.  foreach (@icons) {
  7603.   register_icon( $_->[0], $_->[1] );
  7604.  }
  7605.  return;
  7606. }
  7607.  
  7608. # Add icons
  7609.  
  7610. sub register_icon {
  7611.  my ( $stock_id, $path ) = @_;
  7612.  
  7613.  return unless defined $IconFactory;
  7614.  
  7615.  my $icon;
  7616.  try { $icon = Gtk2::Gdk::Pixbuf->new_from_file($path); }
  7617.  catch {
  7618.   $logger->warn("Unable to load icon `$path': $_");
  7619.   return;
  7620.  };
  7621.  $IconFactory->add( $stock_id, Gtk2::IconSet->new_from_pixbuf($icon) );
  7622.  return;
  7623. }
  7624.  
  7625. # We should clean up after ourselves so that we don't
  7626. # leave dead processes flying around.
  7627. sub kill_subs {
  7628.  
  7629.  foreach ( keys %helperTag ) {
  7630.   killfam 'TERM', ($_);
  7631.   $logger->info("Sending TERM signal to PID $_ and its children");
  7632.  }
  7633.  return;
  7634. }
  7635.  
  7636. # marked page list as saved
  7637.  
  7638. sub mark_pages {
  7639.  my @pages = @_;
  7640.  $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  7641.  foreach (@pages) {
  7642.   $slist->{data}[$_][2]{saved} = TRUE;
  7643.  }
  7644.  $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  7645.  return;
  7646. }
  7647.  
  7648. # Convert all files in temp that are not jpg, png, or tiff to png,
  7649.  
  7650. sub compress_temp {
  7651.  return
  7652.    if (
  7653.   show_message_dialog(
  7654.    $window,     'question',
  7655.    'ok-cancel', $d->get('This operation cannot be undone. Are you sure?')
  7656.   ) ne 'ok'
  7657.    );
  7658.  @undo_buffer    = ();
  7659.  @undo_selection = ();
  7660.  $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
  7661.  $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);
  7662.  
  7663.  for ( @{ $slist->{data} } ) {
  7664.   my ( $signal, $pid );
  7665.   $pid = $slist->to_png(
  7666.    page            => $_->[2],
  7667.    queued_callback => sub {
  7668.     return update_tpbar(@_);
  7669.    },
  7670.    started_callback => sub {
  7671.     my ( $thread, $process, $completed, $total ) = @_;
  7672.     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
  7673.     return TRUE if ( defined $signal );
  7674.    },
  7675.    finished_callback => sub {
  7676.     my ( $new_page, $pending ) = @_;
  7677.     $thbox->hide_all unless $pending;
  7678.     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  7679.    },
  7680.    error_callback => sub {
  7681.     my ($message) = @_;
  7682.     show_message_dialog( $window, 'error', 'close', $message );
  7683.    },
  7684.   );
  7685.  }
  7686.  return;
  7687. }
  7688.  
  7689. # Expand tildes in the filename
  7690.  
  7691. sub expand_tildes {
  7692.  my ($filename) = @_;
  7693.  $filename =~ s{ ^ ~ ( [^/]* ) } {
  7694.   $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($>))[7] )
  7695.  }ex;
  7696.  return $filename;
  7697. }
  7698.  
  7699. # Preferences dialog
  7700.  
  7701. sub preferences {    ## no critic (ProhibitExcessComplexity)
  7702.  
  7703.  if ( defined $windowr ) {
  7704.   $windowr->present;
  7705.   return;
  7706.  }
  7707.  
  7708.  $windowr = Gscan2pdf::Dialog->new(
  7709.   'transient-for'  => $window,
  7710.   title            => $d->get('Preferences'),
  7711.   'hide-on-delete' => TRUE,
  7712.   border_width     => $border_width
  7713.  );
  7714.  my $vbox = $windowr->get('vbox');
  7715.  
  7716.  # Frontends
  7717.  my $hbox = Gtk2::HBox->new;
  7718.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7719.  my $label = Gtk2::Label->new( $d->get('Frontend') );
  7720.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7721.  my @frontends = (
  7722.   [
  7723.    'libsane-perl', $d->get('libsane-perl'),
  7724.    $d->get('Scan using the perl bindings for SANE.')
  7725.   ],
  7726.   [
  7727.    'scanimage', $d->get('scanimage'),
  7728.    $d->get('Scan using the scanimage frontend.')
  7729.   ],
  7730.   [
  7731.    'scanimage-perl',
  7732.    $d->get('scanimage-perl'),
  7733.    $d->get('Scan using the scanimage-perl frontend.')
  7734.   ],
  7735.   [
  7736.    'scanadf-perl', $d->get('scanadf-perl'),
  7737.    $d->get('Scan using the scanadf-perl frontend.')
  7738.   ],
  7739.  );
  7740.  push @frontends,
  7741.    [
  7742.   'scanadf', $d->get('scanadf'),
  7743.   $d->get('Scan using the scanadf frontend.')
  7744.    ],
  7745.    if ( $dependencies{scanadf} );
  7746.  my $combob = combobox_from_array(@frontends);
  7747.  my $cbcsh =
  7748.    Gtk2::CheckButton->new_with_label( $d->get('Cycle SANE handle after scan') );
  7749.  $combob->signal_connect(
  7750.   changed => sub {
  7751.  
  7752.    if ( $frontends[ $combob->get_active ][0] eq 'libsane-perl' ) {
  7753.     $cbcsh->set_sensitive(TRUE);
  7754.    }
  7755.    else {
  7756.     $cbcsh->set_sensitive(FALSE);
  7757.    }
  7758.   }
  7759.  );
  7760.  combobox_set_active( $combob, $SETTING{frontend}, @frontends );
  7761.  $hbox->pack_end( $combob, TRUE, TRUE, 0 );
  7762.  
  7763.  # Cycle SANE handle after scan
  7764.  $cbcsh->set_active( $SETTING{'cycle sane handle'} )
  7765.    if ( defined $SETTING{'cycle sane handle'} );
  7766.  $vbox->pack_start( $cbcsh, TRUE, TRUE, 0 );
  7767.  
  7768.  # Default filename
  7769.  $hbox = Gtk2::HBox->new;
  7770.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7771.  $label = Gtk2::Label->new( $d->get('Default filename for PDFs') );
  7772.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7773.  my $fileentry = Gtk2::Entry->new;
  7774.  $tooltips->set_tip(
  7775.   $fileentry,
  7776.   $d->get(
  7777. "\%a\t author\n\%t\t title\n\%y\t document's year\n\%Y\t today's year\n\%m\t document's month\n\%M\t today's month\n\%d\t document's day\n\%D\t today's day"
  7778.   )
  7779.  );
  7780.  $hbox->add($fileentry);
  7781.  $fileentry->set_text( $SETTING{'default filename'} )
  7782.    if defined( $SETTING{'default filename'} );
  7783.  
  7784.  # scan command prefix
  7785.  $hbox = Gtk2::HBox->new;
  7786.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7787.  $label = Gtk2::Label->new( $d->get('Scan command prefix') );
  7788.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7789.  my $preentry = Gtk2::Entry->new;
  7790.  $hbox->add($preentry);
  7791.  $preentry->set_text( $SETTING{'scan prefix'} )
  7792.    if defined( $SETTING{'scan prefix'} );
  7793.  
  7794.  # Restore window setting
  7795.  my $cbw = Gtk2::CheckButton->new_with_label(
  7796.   $d->get('Restore window settings on startup') );
  7797.  $cbw->set_active(TRUE) if ( $SETTING{'restore window'} );
  7798.  $vbox->pack_start( $cbw, TRUE, TRUE, 0 );
  7799.  
  7800.  # Cache options?
  7801.  my $cbc = Gtk2::CheckButton->new_with_label(
  7802.   $d->get('Cache device-dependent options') );
  7803.  $cbc->set_active(TRUE) if ( $SETTING{'cache options'} );
  7804.  $vbox->pack_start( $cbc, TRUE, TRUE, 0 );
  7805.  
  7806.  # Clear options cache
  7807.  my $button =
  7808.    Gtk2::Button->new( $d->get('Clear device-dependent options cache') );
  7809.  $vbox->pack_start( $button, TRUE, TRUE, 0 );
  7810.  $button->signal_connect(
  7811.   clicked => sub {
  7812.    delete $SETTING{cache} if ( defined $SETTING{cache} );
  7813.   }
  7814.  );
  7815.  
  7816.  # Temporary directory settings
  7817.  $hbox = Gtk2::HBox->new;
  7818.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7819.  $label = Gtk2::Label->new( $d->get('Temporary directory') );
  7820.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7821.  my $tmpentry = Gtk2::Entry->new;
  7822.  $hbox->add($tmpentry);
  7823.  $tmpentry->set_text( dirname($session) );
  7824.  $button = Gtk2::Button->new( $d->get('Browse') );
  7825.  $button->signal_connect(
  7826.   clicked => sub {
  7827.    my $file_chooser = Gtk2::FileChooserDialog->new(
  7828.     $d->get('Select temporary directory'),
  7829.     $windowr, 'select-folder',
  7830.     'gtk-cancel' => 'cancel',
  7831.     'gtk-ok'     => 'ok'
  7832.    );
  7833.    $file_chooser->set_current_folder( $tmpentry->get_text );
  7834.    if ( 'ok' eq $file_chooser->run ) {
  7835.     $tmpentry->set_text( $file_chooser->get_filename );
  7836.    }
  7837.    $file_chooser->destroy;
  7838.   }
  7839.  );
  7840.  $hbox->pack_end( $button, TRUE, TRUE, 0 );
  7841.  
  7842.  # Blank page standard deviation threshold
  7843.  $hbox = Gtk2::HBox->new;
  7844.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7845.  $label = Gtk2::Label->new( $d->get('Blank threshold') );
  7846.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7847.  my $spinbuttonb = Gtk2::SpinButton->new_with_range( 0, 1, 0.001 );
  7848.  $spinbuttonb->set_value( $SETTING{'Blank threshold'} );
  7849.  $tooltips->set_tip( $spinbuttonb,
  7850.   $d->get('Threshold used for selecting blank pages') );
  7851.  $hbox->add($spinbuttonb);
  7852.  
  7853.  # Dark page mean threshold
  7854.  $hbox = Gtk2::HBox->new;
  7855.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7856.  $label = Gtk2::Label->new( $d->get('Dark threshold') );
  7857.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7858.  my $spinbuttond = Gtk2::SpinButton->new_with_range( 0, 1, 0.01 );
  7859.  $spinbuttond->set_value( $SETTING{'Dark threshold'} );
  7860.  $tooltips->set_tip( $spinbuttond,
  7861.   $d->get('Threshold used for selecting dark pages') );
  7862.  $hbox->add($spinbuttond);
  7863.  
  7864.  # OCR output
  7865.  $hbox = Gtk2::HBox->new;
  7866.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7867.  $label = Gtk2::Label->new( $d->get('OCR output') );
  7868.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  7869.  my @array = (
  7870.   [
  7871.    'replace',
  7872.    $d->get('Replace'),
  7873.    $d->get(
  7874.     'Replace the contents of the text buffer with that from the OCR output.')
  7875.   ],
  7876.   [
  7877.    'prepend', $d->get('Prepend'),
  7878.    $d->get('Prepend the OCR output to the text buffer.')
  7879.   ],
  7880.   [
  7881.    'append', $d->get('Append'),
  7882.    $d->get('Append the OCR output to the text buffer.')
  7883.   ],
  7884.  );
  7885.  my $comboo = combobox_from_array(@array);
  7886.  combobox_set_active( $comboo, $SETTING{'OCR output'}, @array );
  7887.  $hbox->pack_end( $comboo, TRUE, TRUE, 0 );
  7888.  
  7889.  # User-defined tools
  7890.  my $frame = Gtk2::Frame->new( $d->get('User-defined tools') );
  7891.  $vbox->pack_start( $frame, TRUE, TRUE, 0 );
  7892.  my $vboxt = Gtk2::VBox->new;
  7893.  $vboxt->set_border_width($border_width);
  7894.  $frame->add($vboxt);
  7895.  for my $tool (@user_defined_tools) {
  7896.   add_user_defined_tool_entry( $vboxt, $tool );
  7897.  }
  7898.  my $abutton = Gtk2::Button->new_from_stock('gtk-add');
  7899.  $vboxt->pack_start( $abutton, TRUE, TRUE, 0 );
  7900.  $abutton->signal_connect(
  7901.   clicked => sub {
  7902.    my %tool = ( cmd => 'my-tool %i %o' );
  7903.    add_user_defined_tool_menu( \%tool );
  7904.    add_user_defined_tool_entry( $vboxt, \%tool );
  7905.    $vboxt->reorder_child( $abutton, -1 );
  7906.    $SETTING{user_defined_tools} = list_user_defined_tools($vboxt);
  7907.   }
  7908.  );
  7909.  
  7910.  # View saved files
  7911.  my $cbv = Gtk2::CheckButton->new_with_label( $d->get('View files on saving') );
  7912.  $cbv->set_active( $SETTING{'view files toggle'} )
  7913.    if ( defined $SETTING{'view files toggle'} );
  7914.  $vbox->pack_start( $cbv, TRUE, TRUE, 0 );
  7915.  
  7916.  # Apply button
  7917.  $hbox = _hbox_with_apply_cancel_buttons(
  7918.   sub {
  7919.    $windowr->hide;
  7920.    if ( $SETTING{frontend} ne $frontends[ $combob->get_active ][0] ) {
  7921.     $SETTING{frontend} = $frontends[ $combob->get_active ][0];
  7922.     if ( $SETTING{frontend} eq 'libsane-perl' ) {
  7923.      undef $windows if ( defined $windows );
  7924.     }
  7925.     else {
  7926.      undef $windows2 if ( defined $windows2 );
  7927.      rescan_options( $device_list[ $combobd->get_active ]{name} )
  7928.        if ( defined $windows );
  7929.     }
  7930.    }
  7931.    $SETTING{'cycle sane handle'} = $cbcsh->get_active;
  7932.    $SETTING{'default filename'}  = $fileentry->get_text;
  7933.    $SETTING{'scan prefix'}       = $preentry->get_text;
  7934.    $SETTING{'cache options'}     = $cbc->get_active;
  7935.    delete $SETTING{cache}
  7936.      if ( defined $SETTING{cache} and not $SETTING{'cache options'} );
  7937.    $SETTING{'restore window'} = $cbw->get_active;
  7938.  
  7939.    my @tmpdirs = File::Spec->splitdir($session);
  7940.    pop @tmpdirs;    # Remove the top level
  7941.    my $tmp = File::Spec->catdir(@tmpdirs);
  7942.  
  7943.    # Expand tildes in the filename
  7944.    my $newdir = expand_tildes( $tmpentry->get_text );
  7945.  
  7946.    if ( $newdir ne $tmp ) {
  7947.     $SETTING{TMPDIR} = $newdir;
  7948.     show_message_dialog(
  7949.      $window,
  7950.      'warning',
  7951.      'close',
  7952.      $d->get(
  7953. 'You will have to restart gscanp2df for changes to the temporary directory to take effect.'
  7954.      )
  7955.     );
  7956.    }
  7957.    $SETTING{'Blank threshold'} = $spinbuttonb->get_value;
  7958.    $SETTING{'Dark threshold'}  = $spinbuttond->get_value;
  7959.    $SETTING{'OCR output'}      = $array[ $comboo->get_active ][0];
  7960.  
  7961.    # Store viewer preferences
  7962.    $SETTING{'view files toggle'} = $cbv->get_active;
  7963.   },
  7964.   sub {
  7965.    $windowr->hide;
  7966.   }
  7967.  );
  7968.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  7969.  $windowr->show_all;
  7970.  return;
  7971. }
  7972.  
  7973. sub _hbox_with_apply_cancel_buttons {
  7974.  my ( $apply_callback, $cancel_callback ) = @_;
  7975.  
  7976.  my $hbox = Gtk2::HBox->new;
  7977.  
  7978.  # Apply button
  7979.  my $button = Gtk2::Button->new_from_stock('gtk-apply');
  7980.  $hbox->pack_start( $button, TRUE, TRUE, 0 );
  7981.  $button->signal_connect( clicked => $apply_callback );
  7982.  
  7983.  # Cancel button
  7984.  $button = Gtk2::Button->new_from_stock('gtk-cancel');
  7985.  $hbox->pack_end( $button, TRUE, TRUE, 0 );
  7986.  $button->signal_connect( clicked => $cancel_callback );
  7987.  return $hbox;
  7988. }
  7989.  
  7990. # Return list of user-defined tools
  7991. sub list_user_defined_tools {
  7992.  my ($vbox) = @_;
  7993.  my @list;
  7994.  for my $hbox ( $vbox->get_children ) {
  7995.   if ( $hbox->isa('Gtk2::HBox') ) {
  7996.    for my $widget ( $hbox->get_children ) {
  7997.     push @list, $widget->get_text if ( $widget->isa('Gtk2::Entry') );
  7998.    }
  7999.   }
  8000.  }
  8001.  return \@list;
  8002. }
  8003.  
  8004. # Add user-defined tool menu item
  8005. sub add_user_defined_tool_menu {
  8006.  my ($tool) = @_;
  8007.  my $action_group = Gtk2::ActionGroup->new("actions");
  8008.  $tool->{name} = "$action_group";
  8009.  my $tool_ui = <<"EOS";
  8010. <ui>
  8011.  <menubar name='MenuBar'>
  8012.   <menu action='Tools'>
  8013.    <menu action='User-defined'>
  8014.     <menuitem action='$tool->{name}'/>
  8015.    </menu>
  8016.   </menu>
  8017.  </menubar>
  8018. </ui>
  8019. EOS
  8020.  
  8021.  # add the basic XML description of the GUI
  8022.  $tool->{merge_id} = $uimanager->add_ui_from_string($tool_ui);
  8023.  
  8024.  my @action_item = (
  8025.  
  8026.   # Fields for each action item:
  8027.   # [name, stock_id, value, label, accelerator, tooltip, callback]
  8028.   [ $tool->{name}, undef, $tool->{cmd}, undef, undef, \&user_defined_tool ],
  8029.  );
  8030.  $action_group->add_actions( \@action_item, undef );
  8031.  
  8032.  # Add the actiongroup to the uimanager
  8033.  $uimanager->insert_action_group( $action_group, 0 );
  8034.  $tool->{action_group} = $action_group;
  8035.  return;
  8036. }
  8037.  
  8038. # Add user-defined tool entry
  8039. sub add_user_defined_tool_entry {
  8040.  my ( $vbox, $tool ) = @_;
  8041.  my $hbox = Gtk2::HBox->new;
  8042.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  8043.  my $entry = Gtk2::Entry->new;
  8044.  $entry->set_text( $tool->{cmd} );
  8045.  $entry->signal_connect(
  8046.   changed => sub {
  8047.    my $action = $tool->{action_group}->get_action( $tool->{name} );
  8048.    $action->set( 'label', $entry->get_text );
  8049.    $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
  8050.    ()    # this callback must return either 2 or 0 items.
  8051.   }
  8052.  );
  8053.  
  8054.  $tooltips->set_tip(
  8055.   $entry,
  8056.   $d->get(
  8057. "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"
  8058.   )
  8059.  );
  8060.  $hbox->pack_start( $entry, TRUE, TRUE, 0 );
  8061.  my $button = Gtk2::Button->new;
  8062.  $button->set_image( Gtk2::Image->new_from_stock( 'gtk-delete', 'button' ) );
  8063.  $button->signal_connect(
  8064.   clicked => sub {
  8065.    $uimanager->remove_ui( $tool->{merge_id} );
  8066.    $hbox->destroy;
  8067.    $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
  8068.   }
  8069.  );
  8070.  $hbox->pack_end( $button, FALSE, FALSE, 0 );
  8071.  $hbox->show_all;
  8072.  return;
  8073. }
  8074.  
  8075. sub properties {
  8076.  
  8077.  if ( defined $windowp ) {
  8078.   $windowp->present;
  8079.   return;
  8080.  }
  8081.  
  8082.  $windowp = Gscan2pdf::Dialog->new(
  8083.   'transient-for'  => $window,
  8084.   title            => $d->get('Properties'),
  8085.   'hide-on-delete' => TRUE,
  8086.   border_width     => $border_width
  8087.  );
  8088.  my $vbox = $windowp->get('vbox');
  8089.  
  8090.  my $hbox = Gtk2::HBox->new;
  8091.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  8092.  my $label = Gtk2::Label->new( $d->get('Resolution') );
  8093.  $hbox->pack_start( $label, FALSE, FALSE, 0 );
  8094.  my $entry = Gtk2::Entry->new;
  8095.  $entry->set_width_chars(4);
  8096.  $entry->set_activates_default(TRUE);
  8097.  $entry->signal_connect(
  8098.   'insert-text' => sub {
  8099.    my ( $widget, $string, $len, $position ) = @_;
  8100.  
  8101.    # only allow a valid positive real number
  8102.    for ( split '', $string ) {
  8103.     if (( $_ ne '.' and ( $_ lt '0' or $_ gt '9' ) )
  8104.      or ( $_ eq '.' and $entry->get_text =~ /\./ ) )
  8105.     {
  8106.      $entry->signal_stop_emission_by_name('insert-text');
  8107.      last;
  8108.     }
  8109.    }
  8110.    ()    # this callback must return either 2 or 0 items.
  8111.   }
  8112.  );
  8113.  
  8114.  $entry->set_text( get_selected_properties() );
  8115.  $slist->get_selection->signal_connect(
  8116.   changed => sub {
  8117.    $entry->set_text( get_selected_properties() );
  8118.   }
  8119.  );
  8120.  $hbox->pack_end( $entry, TRUE, TRUE, 0 );
  8121.  
  8122.  $hbox = Gtk2::HBox->new;
  8123.  $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
  8124.  
  8125.  # Apply button
  8126.  my $button = Gtk2::Button->new_from_stock('gtk-apply');
  8127.  $button->can_default(TRUE);
  8128.  $windowp->set_default($button);
  8129.  $hbox->pack_start( $button, TRUE, TRUE, 0 );
  8130.  $button->signal_connect(
  8131.   clicked => sub {
  8132.    $windowp->hide;
  8133.    my $resolution = $entry->get_text;
  8134.    $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
  8135.    for ( $slist->get_selected_indices ) {
  8136.     $slist->{data}[$_][2]{resolution} = $resolution;
  8137.    }
  8138.    $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  8139.   }
  8140.  );
  8141.  
  8142.  # Cancel button
  8143.  $button = Gtk2::Button->new_from_stock('gtk-cancel');
  8144.  $hbox->pack_end( $button, TRUE, TRUE, 0 );
  8145.  $button->signal_connect(
  8146.   clicked => sub {
  8147.    $windowp->hide;
  8148.   }
  8149.  );
  8150.  $windowp->show_all;
  8151.  return;
  8152. }
  8153.  
  8154. # Helper function for properties()
  8155. sub get_selected_properties {
  8156.  my @page       = $slist->get_selected_indices;
  8157.  my $resolution = '';
  8158.  $resolution = $slist->{data}[ shift @page ][2]{resolution} if ( @page > 0 );
  8159.  for (@page) {
  8160.   if ( $slist->{data}[$_][2]{resolution} != $resolution ) {
  8161.    $resolution = '';
  8162.    last;
  8163.   }
  8164.  }
  8165.  
  8166.  # round the value to a sensible number of significant figures
  8167.  return $resolution eq '' ? '' : sprintf( "%g", $resolution );
  8168. }
  8169.  
  8170. # Helper function to display a message dialog, wait for a response, and return it
  8171.  
  8172. sub show_message_dialog {
  8173.  my ( $parent, $type, $buttons, $text ) = @_;
  8174.  my $dialog =
  8175.    Gtk2::MessageDialog->new( $parent, 'destroy-with-parent', $type, $buttons,
  8176.   $text );
  8177.  my $response = $dialog->run;
  8178.  $dialog->destroy;
  8179.  return $response;
  8180. }
  8181.  
  8182. Gtk2->main;
  8183.  
  8184. __END__
  8185.  
  8186. =encoding utf8
  8187.  
  8188. =head1 Name
  8189.  
  8190. gscan2pdf - A GUI to produce PDFs or DjVus from scanned documents
  8191.  
  8192. =for html <p align="center">
  8193.  <img src="http://sourceforge.net/dbimage.php?id=249848" border="1" width="632"
  8194.  height="480" alt="Screenshot" /><br/>Screenshot: Main page v0.9.30</p>
  8195.  
  8196. =head1 Synopsis
  8197.  
  8198. =over
  8199.  
  8200. =item 1. Scan one or several pages in with File/Scan
  8201.  
  8202. =item 2. Create PDF of selected pages with File/Save
  8203.  
  8204. =back
  8205.  
  8206. =head1 Description
  8207.  
  8208. gscan2pdf has the following command-line options:
  8209.  
  8210. =over
  8211.  
  8212. =item --device=<device>
  8213. Specifies the device to use, instead of getting the list of devices from via the SANE API.
  8214. This can be useful if the scanner is on a remote computer which is not broadcasting its existence.
  8215.  
  8216. =item --help
  8217. Displays this help page and exits.
  8218.  
  8219. =item --log=<log file>
  8220. Specifies a file to store logging messages.
  8221.  
  8222. =item --(debug|info|warn|error|fatal)
  8223. Defines the log level. If a log file is specified, this defaults to 'debug', otherwise 'warn'.
  8224.  
  8225. =item --version
  8226. Displays the program version and exits.
  8227.  
  8228. =back
  8229.  
  8230. Scanning is handled with SANE via scanimage.
  8231. PDF conversion is done by PDF::API2.
  8232. TIFF export is handled by libtiff (faster and smaller memory footprint for
  8233. multipage files).
  8234.  
  8235. =head1 Download
  8236.  
  8237. gscan2pdf is available on Sourceforge
  8238. (L<http://sourceforge.net/projects/gscan2pdf/files/gscan2pdf/>).
  8239.  
  8240. =head2 Debian-based
  8241.  
  8242. If you are using Debian, you should find that sid has the latest version already
  8243. packaged.
  8244.  
  8245. If you are using a Ubuntu-based system, just add the following line to your
  8246. "F</etc/apt/sources.list>" file:
  8247.  
  8248. C<deb http://ppa.launchpad.net/jeffreyratcliffe/ubuntu E<lt>releaseE<gt> main>
  8249.  
  8250. C<deb-src http://ppa.launchpad.net/jeffreyratcliffe/ubuntu E<lt>releaseE<gt> main>
  8251.  
  8252. where C<E<lt>releaseE<gt>> is the version of Ubuntu you are using.
  8253.  
  8254. If you are you are using Synaptic, then use menu
  8255. I<Edit/Reload Package Information>, search for gscan2pdf in the package list,
  8256. and lo and behold, you can install the nice shiny new version automatically.
  8257.  
  8258. From the command line:
  8259.  
  8260. C<apt-get update>
  8261.  
  8262. C<apt-get install gscan2pdf>
  8263.  
  8264. If you add the key for my ppa to your list of trusted keys, then you will no longer get
  8265. the "not authenticated" warnings. Fetch the key:
  8266.  
  8267. C<gpg --keyserver keyserver.ubuntu.com --recv-keys 5BC3E400>
  8268.  
  8269. Then add it to the apt keyring:
  8270.  
  8271. C<gpg --export --armor 5BC3E400 | sudo apt-key add ->
  8272.  
  8273. =head2 RPMs
  8274.  
  8275. Download the rpm from Sourceforge, and then install it with
  8276. C<rpm -i gscan2pdf-version.rpm>
  8277.  
  8278. =head2 From source
  8279.  
  8280. The source is hosted in the files section of the gscan2pdf project on
  8281. Sourceforge (L<http://sourceforge.net/projects/gscan2pdf/files/>).
  8282.  
  8283. =head2 From the repository
  8284.  
  8285. gscan2pdf uses Git for its Revision Control System. You can browse the
  8286. tree at L<http://sourceforge.net/p/gscan2pdf/code/>.
  8287.  
  8288. Git users can clone the complete tree with
  8289. C<git clone git://git.code.sf.net/p/gscan2pdf/code>
  8290.  
  8291. =head1 Building gscan2pdf from source
  8292.  
  8293. Having downloaded the source either from a Sourceforge file release, or from the
  8294. Git repository, unpack it if necessary with
  8295. C<tar xvfz gscan2pdf-x.x.x.tar.gz
  8296. cd gscan2pdf-x.x.x>
  8297.  
  8298. C<perl Makefile.PL>, will create the Makefile.
  8299. There is a C<make test>, but this is not machine-dependent, and therefore really
  8300. just for my benefit to make sure I haven't broken the device-dependent options
  8301. parsing routine.
  8302.  
  8303. You can install directly from the source with C<make install>, but building the
  8304. appropriate package for your distribution should be as straightforward as
  8305. C<make debdist> or C<make rpmdist>. However, you will
  8306. additionally need the rpm, devscripts, fakeroot, debhelper and gettext packages.
  8307.  
  8308. =head1 Dependencies
  8309.  
  8310. The list below looks daunting, but all packages are available from any
  8311. reasonable up-to-date distribution. If you are using Synaptic, having installed
  8312. gscan2pdf, locate the gscan2pdf entry in Synaptic, right-click it and you can
  8313. install them under I<Recommends>. Note also that the library names given below
  8314. are the Debian/Ubuntu ones. Those distributions using RPM typically use
  8315. perl(module) where Debian has libmodule-perl.
  8316.  
  8317. =over
  8318.  
  8319. =item Required
  8320.  
  8321. =over
  8322.  
  8323. =item libgtk2.0-0 (>= 2.4)
  8324.  
  8325. The GTK+ graphical user interface library.
  8326.  
  8327. =item libglib-perl (>= 1.100-1)
  8328.  
  8329. Perl interface to the GLib and GObject libraries
  8330.  
  8331. =item libgtk2-perl (>= 1:1.043-1)
  8332.  
  8333. Perl interface to the 2.x series of the Gimp Toolkit library
  8334.  
  8335. =item libgtk2-imageview-perl
  8336.  
  8337. Perl bindings to the gtkimageview widget.
  8338. See L<http://trac.bjourne.webfactional.com/>
  8339.  
  8340. =item libgtk2-ex-simple-list-perl
  8341.  
  8342. A simple interface to Gtk2's complex MVC list widget
  8343.  
  8344. =item liblocale-gettext-perl (>= 1.05)
  8345.  
  8346. Using libc functions for internationalization in Perl
  8347.  
  8348. =item libpdf-api2-perl
  8349.  
  8350. provides the functions for creating PDF documents in Perl
  8351.  
  8352. =item libsane
  8353.  
  8354. API library for scanners
  8355.  
  8356. =item libsane-perl
  8357.  
  8358. Perl bindings for libsane.
  8359.  
  8360. =item libset-intspan-perl
  8361.  
  8362. manages sets of integers
  8363.  
  8364. =item libtiff-tools
  8365.  
  8366. TIFF manipulation and conversion tools
  8367.  
  8368. =item Imagemagick
  8369.  
  8370. Image manipulation programs
  8371.  
  8372. =item perlmagick
  8373.  
  8374. A perl interface to the libMagick graphics routines
  8375.  
  8376. =item sane-utils
  8377.  
  8378. API library for scanners -- utilities.
  8379.  
  8380. =back
  8381.  
  8382. =item Optional
  8383.  
  8384. =over
  8385.  
  8386. =item sane
  8387.  
  8388. scanner graphical frontends. Only required for the scanadf frontend.
  8389.  
  8390. =item libgtk2-ex-podviewer-perl
  8391.  
  8392. Perl Gtk2 widget for displaying Plain Old Documentation (POD). Not required if
  8393. you don't need the gscan2pdf documentation (which is anyway repeated on the
  8394. website).
  8395.  
  8396. =item unpaper
  8397.  
  8398. post-processing tool for scanned pages. See L<http://unpaper.berlios.de/>.
  8399.  
  8400. =item xdg-utils
  8401.  
  8402. Desktop integration utilities from freedesktop.org. Required for Email as PDF.
  8403. See L<http://portland.freedesktop.org/wiki/>
  8404.  
  8405. =item djvulibre-bin
  8406.  
  8407. Utilities for the DjVu image format. See L<http://djvu.sourceforge.net/>
  8408.  
  8409. =item gocr
  8410.  
  8411. A command line OCR. See L<http://jocr.sourceforge.net/>.
  8412.  
  8413. =item tesseract
  8414.  
  8415. A command line OCR. See L<http://code.google.com/p/tesseract-ocr/>
  8416.  
  8417. =item ocropus
  8418.  
  8419. A command line OCR. See L<http://code.google.com/p/ocropus/>
  8420.  
  8421. =item cuneiform
  8422.  
  8423. A command line OCR. See L<http://launchpad.net/cuneiform-linux>
  8424.  
  8425. =back
  8426.  
  8427. =back
  8428.  
  8429. =head1 Support
  8430.  
  8431. There are two mailing lists for gscan2pdf:
  8432.  
  8433. =over
  8434.  
  8435. =item gscan2pdf-announce
  8436.  
  8437. A low-traffic list for announcements, mostly of new releases. You can subscribe
  8438. at L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-announce>
  8439.  
  8440. =item gscan2pdf-help
  8441.  
  8442. General support, questions, etc.. You can subscribe at
  8443. L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-help>
  8444.  
  8445. =back
  8446.  
  8447. =head1 Reporting bugs
  8448.  
  8449. Before reporting bugs, please read the L<"FAQs"> section.
  8450.  
  8451. Please report any bugs found, preferably against the Debian package[1][2].
  8452. You do not need to be a Debian user, or set up an account to do this.
  8453.  
  8454. =over
  8455.  
  8456. =item 1. http://packages.debian.org/sid/gscan2pdf
  8457.  
  8458. =item 2. http://www.debian.org/Bugs/
  8459.  
  8460. =back
  8461.  
  8462. Alternatively, there is a bug tracker for the gscan2pdf project on
  8463. Sourceforge (L<http://sourceforge.net/p/gscan2pdf/_list/tickets?source=navbar>).
  8464.  
  8465. Please include the log file created by C<gscan2pdf --log=log> with any new bug report.
  8466.  
  8467. =head1 Translations
  8468.  
  8469. gscan2pdf has already been partly translated several languages.
  8470. If you would like to contribute to an existing or new translation, please check
  8471. out Rosetta: L<https://translations.launchpad.net/gscan2pdf>
  8472.  
  8473. Note that the translations for the scanner options are taken
  8474. directly from sane-backends. If you would like to contribute to these, you can
  8475. do so either at contact the sane-devel mailing list
  8476. ([email protected]) and have a look at the po/ directory in
  8477. the source code L<http://www.sane-project.org/cvs.html>.
  8478.  
  8479. Alternatively, Ubuntu has its own translation project. For the 9.04 release, the
  8480. translations are available at
  8481. L<https://translations.launchpad.net/ubuntu/jaunty/+source/sane-backends/+pots/sane-backends>
  8482.  
  8483. =head1 Menus
  8484.  
  8485. =head2 File
  8486.  
  8487. =head3 New
  8488.  
  8489. Clears the page list.
  8490.  
  8491. =head3 Open
  8492.  
  8493. Opens any format that imagemagick supports. PDFs will have their embedded
  8494. images extracted and imported one per page.
  8495.  
  8496. =head3 Scan
  8497.  
  8498. Sets options before scanning via SANE.
  8499.  
  8500. =head4 Device
  8501.  
  8502. Chooses between available scanners.
  8503.  
  8504. =head4 # Pages
  8505.  
  8506. Selects the number of pages, or all pages to scan.
  8507.  
  8508. =head4 Source document
  8509.  
  8510. Selects between single sided or double sides pages.
  8511.  
  8512. This affects the page numbering.
  8513. Single sided scans are numbered consecutively.
  8514. Double sided scans are incremented (or decremented, see below) by 2, i.e. 1, 3,
  8515. 5, etc..
  8516.  
  8517. =head4 Side to scan
  8518.  
  8519. If double sided is selected above, assuming a non-duplex scanner, i.e. a
  8520. scanner that cannot automatically scan both sides of a page, this determines
  8521. whether the page number is incremented or decremented by 2.
  8522.  
  8523. To scan both sides of three pages, i.e. 6 sides:
  8524.  
  8525. =over
  8526.  
  8527. =item 1. Select:
  8528.  
  8529. # Pages = 3 (or "all" if your scanner can detect when it is out of paper)
  8530.  
  8531. Double sided
  8532.  
  8533. Facing side
  8534.  
  8535. =item 2. Scans sides 1, 3 & 5.
  8536.  
  8537. =item 3. Put pile back with scanner ready to scan back of last page.
  8538.  
  8539. =item 4. Select:
  8540.  
  8541. # Pages = 3 (or "all" if your scanner can detect when it is out of paper)
  8542.  
  8543. Double sided
  8544.  
  8545. Reverse side
  8546.  
  8547. =item 5. Scans sides 6, 4 & 2.
  8548.  
  8549. =item 6. gscan2pdf automatically sorts the pages so that they appear in the
  8550. correct order.
  8551.  
  8552. =back
  8553.  
  8554. =head4 Device-dependent options
  8555.  
  8556. These, naturally, depend on your scanner.
  8557. They can include
  8558.  
  8559. =over
  8560.  
  8561. =item Page size.
  8562.  
  8563. =item Mode (colour/black & white/greyscale)
  8564.  
  8565. =item Resolution (in PPI)
  8566.  
  8567. =item Batch-scan
  8568.  
  8569. Guarantees that a "no documents" condition will be returned after the last
  8570. scanned page, to prevent endless flatbed scans after a batch scan.
  8571.  
  8572. =item Wait-for-button/Button-wait
  8573.  
  8574. After sending the scan command, wait until the button on the scanner is pressed
  8575. before actually starting the scan process.
  8576.  
  8577. =item Source
  8578.  
  8579. Selects the document source.
  8580. Possible options can include Flatbed or ADF.
  8581. On some scanners, this is the only way of generating an out-of-documents signal.
  8582.  
  8583. =back
  8584.  
  8585. =head3 Save
  8586.  
  8587. Saves the selected or all pages as a PDF, DjVu, TIFF, PNG, JPEG, PNM or
  8588. GIF.
  8589.  
  8590. =head4 PDF Metadata
  8591.  
  8592. Metadata are information that are not visible when viewing the PDF, but are
  8593. embedded in the file and so searchable and can be examined, typically with the
  8594. "Properties" option of the PDF viewer.
  8595.  
  8596. The metadata are completely optional, but can also be used to generate the
  8597. filename see preferences for details.
  8598.  
  8599. =head4 DjVu
  8600.  
  8601. Both black and white, and colour images produce better
  8602. compression than PDF. See L<http://www.djvuzone.org/> for more details.
  8603.  
  8604. =head3 Email as PDF
  8605.  
  8606. Attaches the selected or all pages as a PDF to a blank email.
  8607. This requires xdg-email, which is in the xdg-utils package.
  8608. If this is not present, the option is ghosted out.
  8609.  
  8610. =head3 Print
  8611.  
  8612. Prints the selected or all pages.
  8613.  
  8614. =head3 Compress temporary files
  8615.  
  8616. If your temporary ($TMPDIR) directory is getting full, this function can be useful -
  8617. compressing all images at LZW-compressed TIFFs. These require much less space than
  8618. the PNM files that are typically produced by SANE or by importing a PDF.
  8619.  
  8620. =head2 Edit
  8621.  
  8622. =head3 Delete
  8623.  
  8624. Deletes the selected page.
  8625.  
  8626. =head3 Renumber
  8627.  
  8628. Renumbers the pages from 1..n.
  8629.  
  8630. Note that the page order can also be changed by drag and drop in the thumbnail
  8631. view.
  8632.  
  8633. =head3 Select
  8634.  
  8635. The select menus can be used to select, all, even, odd, blank, dark or modified
  8636. pages. Selecting blank or dark pages runs imagemagick to make the decision.
  8637. Selecting modified pages selects those which have modified by threshold,
  8638. unsharp, etc., since the last OCR run was made.
  8639.  
  8640. =head3 Preferences
  8641.  
  8642. The preferences menu item allows the control of the default behviour of various
  8643. functions. Most of these are self-explanatory.
  8644.  
  8645. =head4 Frontend
  8646.  
  8647. gscan2pdf supports two frontends, scanimage and scanadf.
  8648. scanadf support was added when it was realised that scanadf works better than
  8649. scanimage with some scanners. On Debian-based systems, scanadf is in the sane package,
  8650. not, like scanimage, in sane-utils. If scanadf is not present, the option is
  8651. obviously ghosted out.
  8652.  
  8653. In 0.9.27, Perl bindings for SANE were introduced and two further frontends,
  8654. scanimage-perl and scanadf-perl (scanimage and scanadf transliterated from C into
  8655. Perl) were added.
  8656.  
  8657. =head4 Default filename for PDF files
  8658.  
  8659. The following variables are available, which are replaced by the corresponding
  8660. metadata:
  8661.  
  8662.  %a author
  8663.  %t title
  8664.  %y document's year
  8665.  %Y today's year
  8666.  %m document's month
  8667.  %M today's month
  8668.  %d document's day
  8669.  %D today's day
  8670.  
  8671. =head2 View
  8672.  
  8673. =head3 Zoom 100%
  8674.  
  8675. Zooms to 1:1. How this appears depends on the desktop resolution.
  8676.  
  8677. =head3 Zoom to fit
  8678.  
  8679. Scales the view such that all the page is visible.
  8680.  
  8681. =head3 Zoom in
  8682.  
  8683. =head3 Zoom out
  8684.  
  8685. =head3 Rotate 90 clockwise
  8686.  
  8687. The rotate options require the package imagemagick and, if this is not present,
  8688. are ghosted out.
  8689.  
  8690. =head3 Rotate 180
  8691.  
  8692. =head3 Rotate 90 anticlockwise
  8693.  
  8694. =head2 Tools
  8695.  
  8696. =head3 Threshold
  8697.  
  8698. Changes all pixels darker than the given value to black; all others become
  8699. white.
  8700.  
  8701. =head3 Unsharp mask
  8702.  
  8703. The unsharp option sharpens an image. The image is convolved with a Gaussian
  8704. operator of the given radius and standard deviation (sigma). For reasonable
  8705. results, radius should be larger than sigma. Use a radius of 0 to have the
  8706. method select a suitable radius.
  8707.  
  8708. =head3 Crop
  8709.  
  8710. =head3 unpaper
  8711.  
  8712. unpaper (see L<http://unpaper.berlios.de/>) is a utility for cleaning up a scan.
  8713.  
  8714. =head3 OCR (Optical Character Recognition)
  8715.  
  8716. The gocr, tesseract, ocropus or cuneiform utilities are used to produce text from
  8717. an image.
  8718.  
  8719. There is an OCR output buffer for each page and is embedded as
  8720. plain text behind the scanned image in the PDF
  8721. produced. This way, Beagle can index (i.e. search) the plain text.
  8722.  
  8723. In DjVu files, the OCR output buffer is embedded in the hidden text layer.
  8724. Thus these can also be indexed by Beagle.
  8725.  
  8726. There is an interesting review of OCR software at
  8727. L<http://web.archive.org/web/20080529012847/http://groundstate.ca/ocr>.
  8728. An important conclusion was that 400ppi is necessary for decent results.
  8729.  
  8730. Up to v2.04, the only way to tell which languages were available to tesseract
  8731. was to look for the language files. Therefore, gscan2pdf checks the path
  8732. returned by:
  8733.  
  8734. tesseract '' '' -l ''
  8735.  
  8736. If there are no language files in the above location, then gscan2pdf
  8737. assumes that tesseract v1.0 is installed, which had no language files.
  8738.  
  8739. =head3 Variables for user-defined tools
  8740.  
  8741. The following variables are available:
  8742.  
  8743.  %i input filename
  8744.  %o output filename
  8745.  %r resolution
  8746.  
  8747. An image can be modified in-place by just specifying %i.
  8748.  
  8749.  
  8750. =head1 FAQs
  8751.  
  8752. =head2 Why isn't option xyz available in the scan window?
  8753.  
  8754. Possibly because SANE or your scanner doesn't support it.
  8755.  
  8756. If an option listed in the output of C<scanimage --help> that you would like to
  8757. use isn't available, send me the output and I will look at implementing it.
  8758.  
  8759. =head2 I've only got an old flatbed scanner with no automatic sheetfeeder.
  8760. How do I scan a multipage document?
  8761.  
  8762. If you are lucky, you have an option like Wait-for-button or Button-wait, where
  8763. the scanner will wait for you to press the scan button on the device before it
  8764. starts the scan, allowing you to scan multiple pages without touching the
  8765. computer.
  8766.  
  8767. Otherwise, you have to set the number of pages to scan to 1 and hit the scan
  8768. button on the scan window for each page.
  8769.  
  8770. =head2 Why is option xyz ghosted out?
  8771.  
  8772. Probably because the package required for that option is not installed.
  8773. Email as PDF requires xdg-email (xdg-utils), unpaper and the rotate options
  8774. require imagemagick.
  8775.  
  8776. =head2 Why can I not scan from the flatbed of my HP scanner?
  8777.  
  8778. Generally for HP scanners with an ADF, to scan from the flatbed, you should
  8779. set "# Pages" to "1", and possibly "Batch scan" to "No".
  8780.  
  8781. =head2 When I update gscan2pdf using the Update Manager in Ubuntu, why is the list of changes never displayed?
  8782.  
  8783. As far as I can tell, this is pulled from changelogs.ubuntu.com, and therefore
  8784. only the changelogs from official Ubuntu builds are displayed.
  8785.  
  8786. =head2 Why can gscan2pdf not find my scanner?
  8787.  
  8788. If your scanner is not connected directly to the machine on which you are
  8789. running gscan2pdf and you have not installed the SANE daemon, saned,
  8790. gscan2pdf cannot automatically find it. In this case, you can specify the
  8791. scanner device on the command line:
  8792.  
  8793. C<gscan2pdf --device <device>>
  8794.  
  8795. =head1 See Also
  8796.  
  8797. Xsane
  8798. http://scantailor.sourceforge.net/
  8799.  
  8800. =head1 Author
  8801.  
  8802. Jeffrey Ratcliffe (ra28145 at users dot sf dot net)
  8803.  
  8804. =head1 Thanks to
  8805.  
  8806. =over
  8807.  
  8808. =item *
  8809. all the people who have sent patches, translations, bugs and feedback.
  8810.  
  8811. =item *
  8812. the GTK2 project for a most excellent graphics toolkit.
  8813.  
  8814. =item *
  8815. the Gtk2-Perl project for their superb Perl bindings for GTK2.
  8816.  
  8817. =item *
  8818. The SANE project for scanner access
  8819.  
  8820. =item *
  8821. Björn Lindqvist for the gtkimageview widget
  8822.  
  8823. =item *
  8824. Sourceforge for hosting the project.
  8825.  
  8826. =back
  8827.  
  8828. =for html <hr />
  8829. <a href="http://sourceforge.net/projects/gscan2pdf">
  8830. <img src="http://sflogo.sourceforge.net/sflogo.php?group_id=174140&amp;type=14" width="150" height="40" alt="Get gscan2pdf at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
  8831. <a href="http://sourceforge.net/p/gscan2pdf/donate/">
  8832. <img src="http://sourceforge.net/images/project-support.jpg" width="88" height="32" border="0" alt="Support This Project"></a>
  8833.  
  8834. =cut
Advertisement
Add Comment
Please, Sign In to add comment