1package Gscan2pdf::Frontend::Image_Sane;
2
3use strict;
4use warnings;
5use feature 'switch';
6no if $] >= 5.018, warnings => 'experimental::smartmatch';
7
8use threads;
9use threads::shared;
10use Thread::Queue;
11use Storable qw(freeze thaw);    # For cloning the options cache
12use Try::Tiny;
13use Data::Dumper;
14$Data::Dumper::Sortkeys = 1;
15use Glib qw(TRUE FALSE);
16use Image::Sane ':all';
17use Data::UUID;
18use File::Temp;                  # To create temporary files
19use Readonly;
20Readonly my $BUFFER_SIZE    => ( 32 * 1024 );     # default size
21Readonly my $_POLL_INTERVAL => 100;               # ms
22Readonly my $_8_BIT         => 8;
23Readonly my $MAXVAL_8_BIT   => 2**$_8_BIT - 1;
24Readonly my $_16_BIT        => 16;
25Readonly my $MAXVAL_16_BIT  => 2**$_16_BIT - 1;
26Readonly my $LARGE_STATUS   => 99;
27Readonly my $NOT_FOUND      => -1;
28my $uuid_object = Data::UUID->new;
29my $EMPTY       = q{};
30
31our $VERSION = '2.12.4';
32
33my ( $prog_name, $logger, %callback, $_self );
34
35sub setup {
36    ( my $class, $logger ) = @_;
37    $_self     = {};
38    $prog_name = Glib::get_application_name;
39
40    $_self->{requests} = Thread::Queue->new;
41    $_self->{return}   = Thread::Queue->new;
42
43    # $_self->{device_handle} explicitly not shared
44    share $_self->{abort_scan};
45    share $_self->{scan_progress};
46
47    $_self->{thread} = threads->new( \&_thread_main, $_self );
48    return;
49}
50
51sub _enqueue_request {
52    my ( $action, $data ) = @_;
53    my $sentinel : shared = 0;
54    $_self->{requests}->enqueue(
55        {
56            action   => $action,
57            sentinel => \$sentinel,
58            ( $data ? %{$data} : () )
59        }
60    );
61    return \$sentinel;
62}
63
64sub _monitor_process {
65    my ( $sentinel, $uuid ) = @_;
66
67    my $started;
68    Glib::Timeout->add(
69        $_POLL_INTERVAL,
70        sub {
71            if ( ${$sentinel} == 2 ) {
72                if ( not $started ) {
73                    if ( defined $callback{$uuid}{started} ) {
74                        $callback{$uuid}{started}->();
75                        delete $callback{$uuid}{started};
76                    }
77                    $started = 1;
78                }
79                check_return_queue();
80                return Glib::SOURCE_REMOVE;
81            }
82            elsif ( ${$sentinel} == 1 ) {
83                if ( not $started ) {
84                    if ( defined $callback{$uuid}{started} ) {
85                        $callback{$uuid}{started}->();
86                        delete $callback{$uuid}{started};
87                    }
88                    $started = 1;
89                }
90                if ( defined $callback{$uuid}{running} ) {
91                    $callback{$uuid}{running}->();
92                }
93                return Glib::SOURCE_CONTINUE;
94            }
95        }
96    );
97    return;
98}
99
100sub quit {
101    _enqueue_request('quit');
102    if ( defined $_self->{thread} ) {
103        $_self->{thread}->join();
104        $_self->{thread} = undef;
105        Image::Sane::_exit();    ## no critic (ProtectPrivateSubs)
106    }
107    return;
108}
109
110sub get_devices {
111    my ( $class, $started_callback, $running_callback, $finished_callback ) =
112      @_;
113
114    my $uuid = $uuid_object->create_str;
115    $callback{$uuid}{started}  = $started_callback;
116    $callback{$uuid}{running}  = $running_callback;
117    $callback{$uuid}{finished} = $finished_callback;
118    my $sentinel = _enqueue_request( 'get-devices', { uuid => $uuid } );
119    _monitor_process( $sentinel, $uuid );
120    return;
121}
122
123sub is_connected {
124    return defined $_self->{device_name};
125}
126
127sub device {
128    return $_self->{device_name};
129}
130
131sub open_device {
132    my ( $class, %options ) = @_;
133
134    my $uuid = $uuid_object->create_str;
135    $callback{$uuid}{started}  = $options{started_callback};
136    $callback{$uuid}{running}  = $options{running_callback};
137    $callback{$uuid}{finished} = sub {
138        $_self->{device_name} = $options{device_name};
139        $options{finished_callback}->();
140    };
141    $callback{$uuid}{error} = $options{error_callback};
142    my $sentinel =
143      _enqueue_request( 'open',
144        { uuid => $uuid, device_name => $options{device_name} } );
145    _monitor_process( $sentinel, $uuid );
146    return;
147}
148
149sub close_device {
150    my ( $class, %options ) = @_;
151
152    my $uuid = $uuid_object->create_str;
153    $callback{$uuid}{started}  = $options{started_callback};
154    $callback{$uuid}{running}  = $options{running_callback};
155    $callback{$uuid}{finished} = sub {
156        $_self->{device_name} = $options{device_name};
157        $options{finished_callback}->();
158    };
159    $callback{$uuid}{error} = $options{error_callback};
160    my $sentinel =
161      _enqueue_request( 'close',
162        { uuid => $uuid, device_name => $options{device_name} } );
163    _monitor_process( $sentinel, $uuid );
164    return;
165}
166
167sub find_scan_options {
168    my (
169        $class,             $started_callback, $running_callback,
170        $finished_callback, $error_callback
171    ) = @_;
172
173    my $uuid = $uuid_object->create_str;
174    $callback{$uuid}{started}  = $started_callback;
175    $callback{$uuid}{running}  = $running_callback;
176    $callback{$uuid}{finished} = $finished_callback;
177    $callback{$uuid}{error}    = $error_callback;
178    my $sentinel = _enqueue_request( 'get-options', { uuid => $uuid } );
179    _monitor_process( $sentinel, $uuid );
180    return;
181}
182
183sub set_option {
184    my ( $class, %options ) = @_;
185
186    my $uuid = $uuid_object->create_str;
187    $callback{$uuid}{started}  = $options{started_callback};
188    $callback{$uuid}{running}  = $options{running_callback};
189    $callback{$uuid}{finished} = $options{finished_callback};
190    $callback{$uuid}{error}    = $options{error_callback};
191    my $sentinel = _enqueue_request(
192        'set-option',
193        {
194            index => $options{index},
195            value => $options{value},
196            uuid  => $uuid,
197        }
198    );
199    _monitor_process( $sentinel, $uuid );
200    return;
201}
202
203sub scan_page {
204    my ( $class, %options ) = @_;
205
206    $_self->{abort_scan}    = 0;
207    $_self->{scan_progress} = 0;
208    my $uuid = $uuid_object->create_str;
209    $callback{$uuid}{started}  = $options{started_callback};
210    $callback{$uuid}{running}  = $options{running_callback};
211    $callback{$uuid}{error}    = $options{error_callback};
212    $callback{$uuid}{finished} = $options{finished_callback};
213    my $sentinel = _enqueue_request( 'scan-page',
214        { uuid => $uuid, path => "$options{path}" } );
215    _monitor_process( $sentinel, $uuid );
216    return;
217}
218
219sub scan_page_finished_callback {
220    my ( $status, $path, $n_scanned, %options ) = @_;
221    if (    defined $options{new_page_callback}
222        and not $_self->{abort_scan}
223        and ( $status == SANE_STATUS_GOOD or $status == SANE_STATUS_EOF ) )
224    {
225        $options{new_page_callback}->( $status, $path, $options{start} );
226    }
227
228    # Stop the process unless everything OK and more scans required
229    if (
230           $_self->{abort_scan}
231        or ( $options{npages} and $n_scanned >= $options{npages} )
232        or (    $status != SANE_STATUS_GOOD
233            and $status != SANE_STATUS_EOF )
234      )
235    {
236        if ( $_self->{abort_scan} ) { unlink $path }
237        _enqueue_request( 'cancel', { uuid => $uuid_object->create_str } );
238        if ( _scanned_enough_pages( $status, $options{npages}, $n_scanned ) ) {
239            if ( defined $options{finished_callback} ) {
240                $options{finished_callback}->();
241            }
242        }
243        else {
244            if ( defined $options{error_callback} ) {
245                $options{error_callback}->( Image::Sane::strstatus($status) );
246            }
247        }
248        return;
249    }
250    elsif ( $options{cancel_between_pages} ) {
251        _enqueue_request( 'cancel', { uuid => $uuid_object->create_str } );
252    }
253
254    if ( not defined $options{step} ) { $options{step} = 1 }
255    $options{start} += $options{step};
256    Gscan2pdf::Frontend::Image_Sane->scan_page(
257        path => File::Temp->new(
258            DIR    => $options{dir},
259            SUFFIX => '.pnm',
260            UNLINK => FALSE,
261        ),
262        started_callback => $options{started_callback},
263        running_callback => sub {
264            $options{running_callback}->( $_self->{scan_progress} );
265        },
266        error_callback    => $options{error_callback},
267        finished_callback => sub {
268            my ( $new_path, $new_status ) = @_;
269            scan_page_finished_callback( $new_status, $new_path, ++$n_scanned,
270                %options );
271        },
272    );
273    return;
274}
275
276sub scan_pages {
277    my ( $class, %options ) = @_;
278
279    my $num_pages_scanned = 0;
280    Gscan2pdf::Frontend::Image_Sane->scan_page(
281        path => File::Temp->new(
282            DIR    => $options{dir},
283            SUFFIX => '.pnm',
284            UNLINK => FALSE,
285        ),
286        started_callback => $options{started_callback},
287        running_callback => sub {
288            if ( defined $options{running_callback} ) {
289                $options{running_callback}->( $_self->{scan_progress} );
290            }
291        },
292        error_callback    => $options{error_callback},
293        finished_callback => sub {
294            my ( $path, $status ) = @_;
295            scan_page_finished_callback( $status, $path, ++$num_pages_scanned,
296                %options );
297        },
298    );
299    return;
300}
301
302sub _scanned_enough_pages {
303    my ( $status, $nrequired, $ndone ) = @_;
304    return (
305             $status == SANE_STATUS_GOOD
306          or $status == SANE_STATUS_EOF
307          or ( $status == SANE_STATUS_NO_DOCS
308            and ( $nrequired == 0 or $nrequired < $ndone ) )
309    );
310}
311
312# Flag the scan routine to abort
313
314sub cancel_scan {
315    my ( $self, $callback ) = @_;
316
317    # Empty process queue first to stop any new process from starting
318    $logger->info('Emptying process queue');
319    while ( $_self->{requests}->dequeue_nb ) { }
320
321    # Then send the thread a cancel signal
322    $_self->{abort_scan} = 1;
323
324    my $uuid = $uuid_object->create_str;
325    $callback{$uuid}{cancelled} = $callback;
326
327    # Add a cancel request to ensure the reply is not blocked
328    $logger->info('Requesting cancel');
329    my $sentinel = _enqueue_request( 'cancel', { uuid => $uuid } );
330    _monitor_process( $sentinel, $uuid );
331    return;
332}
333
334sub _thaw_deref {
335    my ($ref) = @_;
336    if ( defined $ref ) {
337        $ref = thaw($ref);
338        if ( ref($ref) eq 'SCALAR' ) { $ref = ${$ref} }
339    }
340    return $ref;
341}
342
343sub check_return_queue {
344    while ( defined( my $data = $_self->{return}->dequeue_nb() ) ) {
345        if ( not defined $data->{type} ) {
346            $logger->error("Bad data bundle $data in return queue.");
347            next;
348        }
349        if ( not defined $data->{uuid} ) {
350            $logger->error('Bad uuid in return queue.');
351            next;
352        }
353
354        # if we have pressed the cancel button, ignore everything in the returns
355        # queue until it flags 'cancelled'.
356        if ( $_self->{cancel} ) {
357            if ( $data->{type} eq 'cancelled' ) {
358                $_self->{cancel} = FALSE;
359                if ( defined $callback{ $data->{uuid} }{cancelled} ) {
360                    $callback{ $data->{uuid} }{cancelled}
361                      ->( _thaw_deref( $data->{info} ) );
362                    delete $callback{ $data->{uuid} };
363                }
364            }
365            else {
366                next;
367            }
368        }
369
370        if ( $data->{type} eq 'error' ) {
371            if ( $data->{status} == SANE_STATUS_NO_DOCS ) {
372                $data->{type} = 'finished';
373            }
374            else {
375                if ( defined $callback{ $data->{uuid} }{error} ) {
376                    $callback{ $data->{uuid} }{error}
377                      ->( $data->{message}, $data->{status} );
378                    delete $callback{ $data->{uuid} };
379                }
380                return Glib::SOURCE_CONTINUE;
381            }
382        }
383        if ( $data->{type} eq 'finished' ) {
384            if ( defined $callback{ $data->{uuid} }{started} ) {
385                $callback{ $data->{uuid} }{started}->();
386            }
387            if ( defined $callback{ $data->{uuid} }{finished} ) {
388                if ( $data->{process} eq 'set-option' ) {
389                    $callback{ $data->{uuid} }{finished}
390                      ->( $data->{info}, $data->{status} );
391                }
392                else {
393                    $callback{ $data->{uuid} }{finished}
394                      ->( _thaw_deref( $data->{info} ), $data->{status} );
395                }
396                delete $callback{ $data->{uuid} };
397            }
398        }
399    }
400    return Glib::SOURCE_CONTINUE;
401}
402
403sub _log2 {
404    my ($n) = @_;
405    return log($n) / log 2;
406}
407
408sub decode_info {
409    my ($info) = @_;
410    if ( $info == 0 ) { return 'none' }
411    my @opts =
412      qw(SANE_INFO_INEXACT SANE_INFO_RELOAD_OPTIONS SANE_INFO_RELOAD_PARAMS);
413    my @this;
414    my $n = _log2($info);
415    if ( $n > int $n ) {
416        $n = int($n) + 1;
417    }
418    my $i = @opts;
419    while ( $n > $i ) {
420        if ( $info >= 2**( $n - 1 ) ) {
421            push @this, q{?};
422            $info -= 2**( $n - 1 );
423        }
424        --$n;
425    }
426    while ( $n > $NOT_FOUND ) {
427        if ( $info >= 2**$n ) {
428            push @this, $opts[$n];
429            $info -= 2**$n;
430        }
431        --$n;
432    }
433    return join ' + ', @this;
434}
435
436sub _thread_main {
437    my ($self) = @_;
438
439    while ( my $request = $self->{requests}->dequeue ) {
440
441        # Signal the sentinel that the request was started.
442        ${ $request->{sentinel} }++;
443
444        given ( $request->{action} ) {
445            when ('quit') { last }
446            when ('get-devices') {
447                _thread_get_devices( $self, $request->{uuid} )
448            }
449            when ('open') {
450                _thread_open_device( $self, $request->{uuid},
451                    $request->{device_name} )
452            }
453            when ('close') {
454                if ( defined( $self->{device_handle} ) ) {
455                    $logger->debug("closing device '$self->{device_name}'");
456                    undef $self->{device_handle};
457                }
458                else {
459                    $logger->debug(
460                        'Ignoring close_device() call - no device open.');
461                }
462            }
463            when ('get-options') {
464                _thread_get_options( $self, $request->{uuid} )
465            }
466            when ('set-option') {
467                _thread_set_option( $self, $request->{uuid}, $request->{index},
468                    $request->{value} )
469            }
470            when ('scan-page') {
471                _thread_scan_page( $self, $request->{uuid}, $request->{path} )
472            }
473            when ('cancel') { _thread_cancel( $self, $request->{uuid} ) }
474            default {
475                $logger->info("Ignoring unknown request $_");
476                next;
477            }
478        }
479
480        # Signal the sentinel that the request was completed.
481        ${ $request->{sentinel} }++;
482    }
483    return;
484}
485
486sub _thread_get_devices {
487    my ( $self, $uuid ) = @_;
488    my @devices;
489    my $status = SANE_STATUS_GOOD;
490    try {
491        @devices = Image::Sane->get_devices;
492    }
493    catch {
494        $status = $_->status;
495    };
496    $self->{return}->enqueue(
497        {
498            type    => 'finished',
499            process => 'get-devices',
500            uuid    => $uuid,
501            info    => freeze( \@devices ),
502            status  => $status,
503        }
504    );
505    return;
506}
507
508sub _thread_throw_error {
509    my ( $self, $uuid, $process, $status, $message ) = @_;
510    $logger->info($message);
511    $self->{return}->enqueue(
512        {
513            type    => 'error',
514            uuid    => $uuid,
515            status  => $status,
516            message => $message,
517            process => $process,
518        }
519    );
520    return;
521}
522
523sub _thread_open_device {
524    my ( $self, $uuid, $device_name ) = @_;
525
526    if ( not defined $device_name or $device_name eq $EMPTY ) {
527        _thread_throw_error( $self, $uuid, 'open-device',
528            SANE_STATUS_ACCESS_DENIED, 'Cannot open undefined device' );
529        return;
530    }
531
532    # close the handle
533    if ( defined( $self->{device_handle} ) ) {
534        undef $self->{device_handle};
535        Image::Sane::_exit();    ## no critic (ProtectPrivateSubs)
536    }
537
538    my $status = SANE_STATUS_GOOD;
539    try {
540        $self->{device_handle} = Image::Sane::Device->open($device_name);
541        $self->{device_name}   = $device_name;
542        $logger->debug("opened device '$self->{device_name}'");
543    }
544    catch {
545        $status = $_->status;
546        _thread_throw_error( $self, $uuid, 'open-device', $status,
547            "opening device '$device_name': " . $_->error );
548    };
549    $self->{return}->enqueue(
550        {
551            type    => 'finished',
552            process => 'open-device',
553            uuid    => $uuid,
554            info    => freeze( \$device_name ),
555            status  => $status,
556        }
557    );
558    return;
559}
560
561sub _thread_get_options {
562    my ( $self, $uuid ) = @_;
563    my @options;
564
565    # We got a device, find out how many options it has:
566    my $status = SANE_STATUS_GOOD;
567    my $num_dev_options;
568    try {
569        $num_dev_options = $self->{device_handle}->get_option(0);
570    }
571    catch {
572        $status = $_->status;
573        _thread_throw_error( $self, $uuid, 'get-options', $status,
574            'unable to determine option count: ' . $_->error );
575    };
576    $logger->debug("Backend reports $num_dev_options options");
577
578    if ( $status == SANE_STATUS_GOOD ) {
579
580        # if we can retrieve at least one good option, then do so.
581        # otherwise return the error message
582        $status = $LARGE_STATUS;
583        for my $i ( 1 .. $num_dev_options - 1 ) {
584            my $opt;
585            try {
586                $opt = $self->{device_handle}->get_option_descriptor($i);
587            }
588            catch {
589                $status = $_->status;
590                _thread_throw_error( $self, $uuid, 'get_option_descriptor',
591                    $status,
592                    "error getting option descriptor $i: " . $_->error );
593            };
594            $options[$i] = $opt;
595            if (
596                $opt->{cap} & SANE_CAP_SOFT_DETECT
597                and not( ( $opt->{cap} & SANE_CAP_INACTIVE )
598                    or ( $opt->{type} == SANE_TYPE_BUTTON )
599                    or ( $opt->{type} == SANE_TYPE_GROUP ) )
600              )
601            {
602                try {
603                    $opt->{val} = $self->{device_handle}->get_option($i);
604                    $status = SANE_STATUS_GOOD;
605                }
606                catch {
607                    if ( $_->status < $status ) { $status = $_->status }
608                    $opt->{cap} = 0;
609                    $logger->warn( "Error getting option $i. ", Dumper($opt) );
610                };
611            }
612        }
613        if ( $status != SANE_STATUS_GOOD ) {
614            _thread_throw_error( $self, $uuid, 'get_option', $status,
615                'no options fetched: ' . $_->error );
616        }
617    }
618    $self->{return}->enqueue(
619        {
620            type    => 'finished',
621            process => 'get-options',
622            uuid    => $uuid,
623            info    => freeze( \@options ),
624            status  => $status,
625        }
626    );
627    return $status;
628}
629
630sub _thread_set_option {
631    my ( $self, $uuid, $index, $value ) = @_;
632    my $opt = $self->{device_handle}->get_option_descriptor($index);
633    if ( $opt->{type} == SANE_TYPE_BOOL and $value eq $EMPTY ) { $value = 0 }
634
635    # FIXME: Stringification to force this SV to have a PV slot.  This seems to
636    # be necessary to get through Sane.pm's value checks.
637    if ( $opt->{type} != SANE_TYPE_BUTTON ) { $value = "$value" }
638
639    my $status = SANE_STATUS_GOOD;
640    my $info;
641    try {
642        $info = $self->{device_handle}->set_option( $index, $value );
643    }
644    catch {
645        $status = $_->status;
646    };
647    if ( $logger->is_info ) {
648        $logger->info(
649                "sane_set_option $index ($opt->{name})"
650              . ( $opt->{type} == SANE_TYPE_BUTTON ? $EMPTY : " to $value" )
651              . " returned status $status ("
652              . Image::Sane::strstatus($status)
653              . ') with info '
654              . (
655                defined $info
656                ? sprintf( '%d (%s)',
657                    $info, Gscan2pdf::Frontend::Image_Sane::decode_info($info) )
658                : 'undefined'
659              )
660        );
661    }
662    $self->{return}->enqueue(
663        {
664            type    => 'finished',
665            process => 'set-option',
666            uuid    => $uuid,
667            status  => $status,
668            info    => $info,
669        }
670    );
671    return;
672}
673
674sub _thread_write_pnm_header {
675    my ( $fh, $format, $width, $height, $depth ) = @_;
676
677    # The netpbm-package does not define raw image data with maxval > 255.
678    # But writing maxval 65535 for 16bit data gives at least a chance
679    # to read the image.
680
681    if (   $format == SANE_FRAME_RED
682        or $format == SANE_FRAME_GREEN
683        or $format == SANE_FRAME_BLUE
684        or $format == SANE_FRAME_RGB )
685    {
686        printf {$fh} "P6\n# SANE data follows\n%d %d\n%d\n", $width, $height,
687          ( $depth > $_8_BIT ) ? $MAXVAL_16_BIT : $MAXVAL_8_BIT;
688    }
689    else {
690        if ( $depth == 1 ) {
691            printf {$fh} "P4\n# SANE data follows\n%d %d\n", $width, $height;
692        }
693        else {
694            printf {$fh} "P5\n# SANE data follows\n%d %d\n%d\n", $width,
695              $height,
696              ( $depth > $_8_BIT ) ? $MAXVAL_16_BIT : $MAXVAL_8_BIT;
697        }
698    }
699    return;
700}
701
702sub _thread_scan_page_to_fh {
703    my ( $device, $fh ) = @_;
704    my $first_frame = 1;
705    my $offset      = 0;
706    my $must_buffer = 0;
707    my ( %image, $status );
708    my @format_name = qw( gray RGB red green blue );
709    my $total_bytes = 0;
710
711    my ( $parm, $last_frame );
712    while ( not $last_frame ) {
713        $status = SANE_STATUS_GOOD;
714        if ( not $first_frame ) {
715            try {
716                $device->start;
717            }
718            catch {
719                $status = $_->status;
720                $logger->info( "$prog_name: sane_start: " . $_->error );
721            };
722            if ( $status != SANE_STATUS_GOOD ) { goto CLEANUP }
723        }
724
725        try {
726            $parm = $device->get_parameters;
727        }
728        catch {
729            $status = $_->status;
730            $logger->info( "$prog_name: sane_get_parameters: " . $_->error );
731        };
732        if ( $status != SANE_STATUS_GOOD ) { goto CLEANUP }
733
734        _log_frame_info( $first_frame, $parm, \@format_name );
735        ( $must_buffer, $offset ) =
736          _initialise_scan( $fh, $first_frame, $parm );
737        my $hundred_percent = _scan_data_size($parm);
738
739        while (1) {
740
741            # Pick up flag from cancel_scan()
742            if ( $_self->{abort_scan} ) {
743                $device->cancel;
744                $logger->info('Scan cancelled');
745                return SANE_STATUS_CANCELLED;
746            }
747
748            my ( $buffer, $len );
749            try {
750                ( $buffer, $len ) = $device->read($BUFFER_SIZE);
751                $total_bytes += $len;
752            }
753            catch {
754                $status = $_->status;
755                $logger->info( "$prog_name: sane_read: " . $_->error );
756            };
757            my $progr = $total_bytes / $hundred_percent;
758            if ( $progr > 1 ) { $progr = 1 }
759            $_self->{scan_progress} = $progr;
760
761            if ( $status != SANE_STATUS_GOOD ) {
762                if ( $parm->{depth} == $_8_BIT ) {
763                    $logger->info(
764                        sprintf "$prog_name: min/max graylevel value = %d/%d",
765                        $MAXVAL_8_BIT, 0 );
766                }
767                if ( $status != SANE_STATUS_EOF ) { return $status }
768                last;
769            }
770
771            if ($must_buffer) {
772                $offset =
773                  _buffer_scan( $offset, $parm, \%image, $len, $buffer );
774            }
775            else {
776                goto CLEANUP if not print {$fh} $buffer;
777            }
778        }
779        $first_frame = 0;
780        $last_frame  = $parm->{last_frame};
781    }
782
783    if ($must_buffer) { _write_buffer_to_fh( $fh, $parm, \%image ) }
784
785  CLEANUP:
786    my $expected_bytes =
787      $parm->{bytes_per_line} * $parm->{lines} * _number_frames($parm);
788    if ( $parm->{lines} < 0 ) { $expected_bytes = 0 }
789    if ( $total_bytes > $expected_bytes and $expected_bytes != 0 ) {
790        $logger->info(
791            sprintf '%s: WARNING: read more data than announced by backend '
792              . '(%u/%u)',
793            $prog_name, $total_bytes, $expected_bytes );
794    }
795    else {
796        $logger->info( sprintf '%s: read %u bytes in total',
797            $prog_name, $total_bytes );
798    }
799    return $status;
800}
801
802sub _thread_scan_page {
803    my ( $self, $uuid, $path ) = @_;
804
805    if ( not defined( $self->{device_handle} ) ) {
806        _thread_throw_error( $self, $uuid, 'scan-page',
807            SANE_STATUS_ACCESS_DENIED,
808            "$prog_name: must open device before starting scan" );
809        return;
810    }
811    my $status = SANE_STATUS_GOOD;
812    try {
813        $self->{device_handle}->start;
814    }
815    catch {
816        $status = $_->status;
817        _thread_throw_error( $self, $uuid, 'scan-page', $status,
818            "$prog_name: sane_start: " . $_->error );
819        unlink $path;
820    };
821    if ( $status != SANE_STATUS_GOOD ) { return }
822
823    my $fh;
824    if ( not open $fh, '>', $path ) {
825        $self->{device_handle}->cancel;
826        _thread_throw_error( $self, $uuid, 'scan-page',
827            SANE_STATUS_ACCESS_DENIED, "Error writing to $path" );
828        return;
829    }
830
831    $status = _thread_scan_page_to_fh( $self->{device_handle}, $fh );
832
833    if ( not close $fh ) {
834        $self->{device_handle}->cancel;
835        _thread_throw_error( $self, $uuid, 'scan-page',
836            SANE_STATUS_ACCESS_DENIED, "Error closing $path" );
837        return;
838    }
839
840    $logger->info( sprintf 'Scanned page %s. (scanner status = %d)',
841        $path, $status );
842
843    if ( $status != SANE_STATUS_GOOD and $status != SANE_STATUS_EOF ) {
844        unlink $path;
845    }
846
847    $self->{return}->enqueue(
848        {
849            type    => 'finished',
850            process => 'scan-page',
851            uuid    => $uuid,
852            status  => $status,
853            info    => freeze( \$path ),
854        }
855    );
856    return;
857}
858
859sub _thread_cancel {
860    my ( $self, $uuid ) = @_;
861    if ( defined $self->{device_handle} ) { $self->{device_handle}->cancel }
862    $self->{return}->enqueue( { type => 'cancelled', uuid => $uuid } );
863    return;
864}
865
866sub _log_frame_info {
867    my ( $first_frame, $parm, $format_name ) = @_;
868    if ($first_frame) {
869        if ( $parm->{lines} >= 0 ) {
870            $logger->info(
871                sprintf "$prog_name: scanning image of size %dx%d pixels at "
872                  . '%d bits/pixel',
873                $parm->{pixels_per_line},
874                $parm->{lines},
875                $_8_BIT * $parm->{bytes_per_line} / $parm->{pixels_per_line}
876            );
877        }
878        else {
879            $logger->info(
880                sprintf "$prog_name: scanning image %d pixels wide and "
881                  . 'variable height at %d bits/pixel',
882                $parm->{pixels_per_line},
883                $_8_BIT * $parm->{bytes_per_line} / $parm->{pixels_per_line}
884            );
885        }
886
887        $logger->info(
888            sprintf "$prog_name: acquiring %s frame",
889            $parm->{format} <= SANE_FRAME_BLUE
890            ? $format_name->[ $parm->{format} ]
891            : 'Unknown'
892        );
893    }
894    return;
895}
896
897sub _initialise_scan {
898    my ( $fh, $first_frame, $parm ) = @_;
899    my ( $must_buffer, $offset );
900    if ($first_frame) {
901        if (   $parm->{format} == SANE_FRAME_RED
902            or $parm->{format} == SANE_FRAME_GREEN
903            or $parm->{format} == SANE_FRAME_BLUE )
904        {
905            if ( $parm->{depth} != $_8_BIT ) {
906                die "Red/Green/Blue frames require depth=$_8_BIT\n";
907            }
908            $must_buffer = 1;
909            $offset      = $parm->{format} - SANE_FRAME_RED;
910        }
911        elsif ( $parm->{format} == SANE_FRAME_RGB ) {
912            if (    ( $parm->{depth} != $_8_BIT )
913                and ( $parm->{depth} != $_16_BIT ) )
914            {
915                die "RGB frames require depth=$_8_BIT or $_16_BIT\n";
916            }
917        }
918        if (   $parm->{format} == SANE_FRAME_RGB
919            or $parm->{format} == SANE_FRAME_GRAY )
920        {
921            if (    ( $parm->{depth} != 1 )
922                and ( $parm->{depth} != $_8_BIT )
923                and ( $parm->{depth} != $_16_BIT ) )
924            {
925                die "Valid depths are 1, $_8_BIT or $_16_BIT\n";
926            }
927            if ( $parm->{lines} < 0 ) {
928                $must_buffer = 1;
929                $offset      = 0;
930            }
931            else {
932                _thread_write_pnm_header( $fh, $parm->{format},
933                    $parm->{pixels_per_line},
934                    $parm->{lines}, $parm->{depth} );
935            }
936        }
937    }
938    else {
939        die "Encountered unknown format\n"
940          if ( $parm->{format} < SANE_FRAME_RED
941            or $parm->{format} > SANE_FRAME_BLUE );
942        $offset = $parm->{format} - SANE_FRAME_RED;
943    }
944    return ( $must_buffer, $offset );
945}
946
947# Return size of final scan (ignoring header)
948
949sub _scan_data_size {
950    my ($parm) = @_;
951    return $parm->{bytes_per_line} * $parm->{lines} * _number_frames($parm);
952}
953
954# Return number of frames
955
956sub _number_frames {
957    my ($parm) = @_;
958    return (
959             $parm->{format} == SANE_FRAME_RGB
960          or $parm->{format} == SANE_FRAME_GRAY
961      )
962      ? 1
963      : 3;    ## no critic (ProhibitMagicNumbers)
964}
965
966# We're either scanning a multi-frame image or the
967# scanner doesn't know what the eventual image height
968# will be (common for hand-held scanners).  In either
969# case, we need to buffer all data before we can write
970# the header
971
972sub _buffer_scan {
973    my ( $offset, $parm, $image, $len, $buffer ) = @_;
974
975    my $number_frames = _number_frames($parm);
976    for ( 0 .. $len - 1 ) {
977        $image->{data}[ $offset + $number_frames * $_ ] = substr $buffer, $_, 1;
978    }
979    $offset += $number_frames * $len;
980    return $offset;
981}
982
983sub _write_buffer_to_fh {
984    my ( $fh, $parm, $image ) = @_;
985    if ( $parm->{lines} > 0 ) {
986        $image->{height} = $parm->{lines};
987    }
988    else {
989        $image->{height} = @{ $image->{data} } / $parm->{bytes_per_line};
990        $image->{height} /= _number_frames($parm);
991    }
992    _thread_write_pnm_header( $fh, $parm->{format}, $parm->{pixels_per_line},
993        $image->{height}, $parm->{depth} );
994    for my $data ( @{ $image->{data} } ) {
995        goto CLEANUP if not print {$fh} $data;
996    }
997    return;
998}
999
10001;
1001
1002__END__
1003