1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# (c) Copyright 2003-2015 HP Development Company, L.P.
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
19#
20# Based on:
21# "sane.py", part of the Python Imaging Library (PIL)
22# http://www.pythonware.com/products/pil/
23# Python wrapper on top of the _sane module, which is in turn a very
24# thin wrapper on top of the SANE library.  For a complete understanding
25# of SANE, consult the documentation at the SANE home page:
26# http://www.mostang.com/sane/ .#
27#
28# Modified to work without PIL by Don Welch
29#
30# (C) Copyright 2003 A.M. Kuchling.  All Rights Reserved
31# (C) Copyright 2004 A.M. Kuchling, Ralph Heinkel  All Rights Reserved
32#
33# Permission to use, copy, modify, and distribute this software and its
34# documentation for any purpose and without fee is hereby granted,
35# provided that the above copyright notice appear in all copies and that
36# both that copyright notice and this permission notice appear in
37# supporting documentation, and that the name of A.M. Kuchling and
38# Ralph Heinkel not be used in advertising or publicity pertaining to
39# distribution of the software without specific, written prior permission.
40#
41# A.M. KUCHLING, R.H. HEINKEL DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
42# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS,
43# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
44# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
45# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
46# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
47# PERFORMANCE OF THIS SOFTWARE.
48# Python wrapper on top of the scanext module, which is in turn a very
49# thin wrapper on top of the SANE library.  For a complete understanding
50# of SANE, consult the documentation at the SANE home page:
51# http://www.mostang.com/sane/ .
52#
53# Original authors: Andrew Kuchling, Ralph Heinkel
54# Modified by: Don Welch, Sarbeswar Meher
55#
56
57# Std Lib
58import scanext
59import threading
60import time
61import os
62
63# Local
64from base.g import *
65from base import utils
66from base.sixext import to_bytes_utf8
67from base.sixext.moves import queue
68
69EVENT_SCAN_CANCELED = 1
70
71TYPE_STR = { scanext.TYPE_BOOL:   "TYPE_BOOL",   scanext.TYPE_INT:    "TYPE_INT",
72             scanext.TYPE_FIXED:  "TYPE_FIXED",  scanext.TYPE_STRING: "TYPE_STRING",
73             scanext.TYPE_BUTTON: "TYPE_BUTTON", scanext.TYPE_GROUP:  "TYPE_GROUP" }
74
75UNIT_STR = { scanext.UNIT_NONE:        "UNIT_NONE",
76             scanext.UNIT_PIXEL:       "UNIT_PIXEL",
77             scanext.UNIT_BIT:         "UNIT_BIT",
78             scanext.UNIT_MM:          "UNIT_MM",
79             scanext.UNIT_DPI:         "UNIT_DPI",
80             scanext.UNIT_PERCENT:     "UNIT_PERCENT",
81             scanext.UNIT_MICROSECOND: "UNIT_MICROSECOND" }
82
83
84MAX_READSIZE = 65536
85
86class Option:
87    """Class representing a SANE option.
88    Attributes:
89    index -- number from 0 to n, giving the option number
90    name -- a string uniquely identifying the option
91    title -- single-line string containing a title for the option
92    desc -- a long string describing the option; useful as a help message
93    type -- type of this option.  Possible values: TYPE_BOOL,
94            TYPE_INT, TYPE_STRING, and so forth.
95    unit -- units of this option.  Possible values: UNIT_NONE,
96            UNIT_PIXEL, etc.
97    size -- size of the value in bytes
98    cap -- capabilities available; CAP_EMULATED, CAP_SOFT_SELECT, etc.
99    constraint -- constraint on values.  Possible values:
100        None : No constraint
101        (min,max,step)  Integer values, from min to max, stepping by
102        list of integers or strings: only the listed values are allowed
103    """
104
105    def __init__(self, args, cur_device):
106        import string
107        self.cur_device = cur_device
108
109        self.index, self.name, self.title, self.desc, self.type, \
110            self.unit, self.size, self.cap, self.constraint = args
111
112        if type(self.name) != type(''):
113            self.name = str(self.name)
114
115    def isActive(self):
116        return scanext.isOptionActive(self.cap)
117
118    def isSettable(self):
119        return scanext.isOptionSettable(self.cap)
120
121    def __repr__(self):
122        if self.isSettable():
123            settable = 'yes'
124        else:
125            settable = 'no'
126
127        if self.isActive():
128            active = 'yes'
129            curValue = self.cur_device.getOption(self.name)
130        else:
131            active = 'no'
132            curValue = '<not available, inactive option>'
133
134
135        return """\nName:      %s
136Cur value: %s
137Index:     %d
138Title:     %s
139Desc:      %s
140Type:      %s
141Unit:      %s
142Constr:    %s
143isActive:    %s
144isSettable:  %s\n""" % (self.name, curValue,
145                      self.index, self.title, self.desc,
146                      TYPE_STR[self.type], UNIT_STR[self.unit],
147                      self.constraint, active, settable)
148        return s
149
150    def limitAndSet(self, value):
151        if value is not None and self.constraint is not None:
152            if type(self.constraint) == type(()):
153                if value < self.constraint[0]:
154                    value = self.constraint[0]
155                    log.warn("Invalid value for %s (%s < min value of %d). Using %d." %
156                        (self.name, self.name, value, value))
157
158                elif value > self.constraint[1]:
159                    value = self.constraint[1]
160                    log.warn("Invalid value for %s (%s > max value of %d). Using %d." %
161                        (self.name, self.name, value, value))
162
163                self.cur_device.setOption(self.name, value)
164
165            elif type(self.constraint) == type([]):
166                if value not in self.constraint:
167                    v = self.constraint[0]
168                    min_dist = sys.maxsize
169                    for x in self.constraint:
170                        if abs(value-x) < min_dist:
171                            min_dist = abs(value-x)
172                            v = x
173
174                    log.warn("Invalid value for %s (%s not in constraint list: %s). Using %d." %
175                        (self.name, self.name, value, ', '.join(self.constraint), v))
176
177                    self.cur_device.setOption(self.name, v)
178
179        else:
180            value = self.cur_device.getOption(self.name)
181
182        return value
183
184
185##class _SaneIterator:
186##    """ intended for ADF scans.
187##    """
188##
189##    def __init__(self, cur_device):
190##        self.cur_device = cur_device
191##
192##    def __iter__(self):
193##        return self
194##
195##    def __del__(self):
196##        self.cur_device.cancelScan()
197##
198##    def next(self):
199##        try:
200##            self.cur_device.startScan()
201##        except error, v:
202##            if v == 'Document feeder out of documents':
203##                raise StopIteration
204##            else:
205##                raise
206##        return self.cur_device.performScan(1)
207
208
209
210
211class ScanDevice:
212    """Class representing a SANE device.
213    Methods:
214    startScan()    -- initiate a scan, using the current settings
215    cancelScan()   -- cancel an in-progress scanning operation
216
217    Also available, but rather low-level:
218    getParameters() -- get the current parameter settings of the device
219    getOptions()    -- return a list of tuples describing all the options.
220
221    Attributes:
222    optlist -- list of option names
223
224    You can also access an option name to retrieve its value, and to
225    set it.  For example, if one option has a .name attribute of
226    imagemode, and scanner is a ScanDevice object, you can do:
227         print scanner.imagemode
228         scanner.imagemode = 'Full frame'
229         scanner.['imagemode'] returns the corresponding Option object.
230    """
231
232    def __init__(self, dev):
233        self.scan_thread = None
234        self.dev = scanext.openDevice(dev)
235        self.options = {}
236        self.__load_options_dict()
237
238
239    def __load_options_dict(self):
240        opts = self.options
241        opt_list = self.dev.getOptions()
242
243        for t in opt_list:
244            o = Option(t, self)
245
246            if o.type != scanext.TYPE_GROUP:
247                opts[o.name] = o
248
249
250    def setOption(self, key, value):
251        opts = self.options
252
253        if key not in opts:
254            opts[key] = value
255            return
256
257        opt = opts[key]
258
259        if opt.type == scanext.TYPE_GROUP:
260            log.error("Groups can't be set: %s" % key)
261
262        if not scanext.isOptionActive(opt.cap):
263            log.error("Inactive option: %s" % key)
264
265        if not scanext.isOptionSettable(opt.cap):
266            log.error("Option can't be set by software: %s" % key)
267
268        if type(value) == int and opt.type == scanext.TYPE_FIXED:
269            # avoid annoying errors of backend if int is given instead float:
270            value = float(value)
271
272        try:
273            self.last_opt = self.dev.setOption(opt.index, value)
274        except scanext.error:
275            log.error("Unable to set option %s to value %s" % (key, value))
276            return
277
278        # do binary AND to find if we have to reload options:
279        if self.last_opt & scanext.INFO_RELOAD_OPTIONS:
280            self.__load_options_dict()
281
282
283    def getOption(self, key):
284        opts = self.options
285
286        if key == 'optlist':
287            return list(opts.keys())
288
289        if key == 'area':
290            return (opts["tl-x"], opts["tl-y"]), (opts["br-x"], opts["br-y"])
291
292        if key not in opts:
293            raise AttributeError('No such attribute: %s' % key)
294
295        opt = opts[key]
296
297        if opt.type == scanext.TYPE_BUTTON:
298            raise AttributeError("Buttons don't have values: %s" % key)
299
300        if opt.type == scanext.TYPE_GROUP:
301            raise AttributeError("Groups don't have values: %s " % key)
302
303        if not scanext.isOptionActive(opt.cap):
304            raise AttributeError('Inactive option: %s' % key)
305
306        return self.dev.getOption(opt.index)
307
308
309    def getOptionObj(self, key):
310        opts = self.options
311        if key in opts:
312            return opts[key]
313
314
315    def getParameters(self):
316        """Return a 6-tuple holding all the current device settings:
317           (format, format_name, last_frame, (pixels_per_line, lines), depth, bytes_per_line)
318
319            - format is the SANE frame type
320            - format is one of 'grey', 'color' (RGB), 'red', 'green', 'blue'.
321            - last_frame [bool] indicates if this is the last frame of a multi frame image
322            - (pixels_per_line, lines) specifies the size of the scanned image (x,y)
323            - lines denotes the number of scanlines per frame
324            - depth gives number of pixels per sample
325        """
326        return self.dev.getParameters()
327
328
329    def getOptions(self):
330        "Return a list of tuples describing all the available options"
331        return self.dev.getOptions()
332
333
334    def startScan(self, byte_format='BGRA', update_queue=None, event_queue=None):
335        """
336            Perform a scan with the current device.
337            Calls sane_start().
338        """
339        if not self.isScanActive():
340            status = self.dev.startScan()
341            self.format, self.format_name, self.last_frame, self.pixels_per_line, \
342            self.lines, self.depth, self.bytes_per_line = self.dev.getParameters()
343
344            self.scan_thread = ScanThread(self.dev, byte_format, update_queue, event_queue)
345            self.scan_thread.scan_active = True
346            self.scan_thread.start()
347            return True, self.lines * self.bytes_per_line, status
348        else:
349            # Already active
350            return False, 0, scanext.SANE_STATUS_DEVICE_BUSY
351
352
353    def cancelScan(self):
354        "Cancel an in-progress scanning operation."
355        return self.dev.cancelScan()
356
357
358    def getScan(self):
359        "Get the output buffer and info about a completed scan."
360        if not self.isScanActive():
361            s = self.scan_thread
362
363            return s.buffer, s.format, s.format_name, s.pixels_per_line, \
364                s.lines, s.depth, s.bytes_per_line, s.pad_bytes, s.total_read, s.total_write
365
366
367    def freeScan(self):
368        "Cleanup the scan file after a completed scan."
369        if not self.isScanActive():
370            s = self.scan_thread
371
372            try:
373                s.buffer.close()
374                os.remove(s.buffer_path)
375            except (IOError, AttributeError):
376                pass
377
378
379    def isScanActive(self):
380        if self.scan_thread is not None:
381            return self.scan_thread.isAlive() and self.scan_thread.scan_active
382        else:
383            return False
384
385
386    def waitForScanDone(self):
387        if self.scan_thread is not None and \
388            self.scan_thread.isAlive() and \
389            self.scan_thread.scan_active:
390
391            try:
392                self.scan_thread.join()
393            except KeyboardInterrupt:
394                pass
395
396
397    def waitForScanActive(self):
398        #time.sleep(0.5)
399        if self.scan_thread is not None:
400            while True:
401                #print self.scan_thread.isAlive()
402                #print self.scan_thread.scan_active
403                if self.scan_thread.isAlive() and \
404                    self.scan_thread.scan_active:
405                    return
406
407                time.sleep(0.1)
408                #print "Waiting..."
409
410
411##    def scanMulti(self):
412##        return _SaneIterator(self)
413
414
415    def closeScan(self):
416        "Close the SANE device after a scan."
417        self.dev.closeScan()
418
419
420
421class ScanThread(threading.Thread):
422    def __init__(self, device, byte_format='BGRA', update_queue=None, event_queue=None):
423        threading.Thread.__init__(self)
424        self.scan_active = True
425        self.dev = device
426        self.update_queue = update_queue
427        self.event_queue = event_queue
428        self.buffer_fd, self.buffer_path = utils.make_temp_file(prefix='hpscan')
429        self.buffer = os.fdopen(self.buffer_fd, "w+b")
430        self.format = -1
431        self.format_name = ''
432        self.last_frame = -1
433        self.pixels_per_line = -1
434        self.lines = -1
435        self.depth = -1
436        self.bytes_per_line = -1
437        self.pad_bytes = -1
438        self.total_read = 0
439        self.byte_format = byte_format
440        self.total_write = 0
441
442
443    def updateQueue(self, status, bytes_read):
444        if self.update_queue is not None:
445            try:
446                status = int(status)
447            except (ValueError, TypeError):
448                status = -1 #scanext.SANE_STATUS_GOOD
449
450            self.update_queue.put((status, bytes_read))
451
452
453
454    def run(self):
455        from base.sixext import to_bytes_utf8
456        #self.scan_active = True
457        self.format, self.format_name, self.last_frame, self.pixels_per_line, \
458            self.lines, self.depth, self.bytes_per_line = self.dev.getParameters()
459
460        log.debug("format=%d" % self.format)
461        log.debug("format_name=%s" % self.format_name)
462        log.debug("last_frame=%d" % self.last_frame)
463        log.debug("ppl=%d" % self.pixels_per_line)
464        log.debug("lines=%d" % self.lines)
465        log.debug("depth=%d" % self.depth)
466        log.debug("bpl=%d" % self.bytes_per_line)
467        log.debug("byte_format=%s" % self.byte_format)
468
469        w = self.buffer.write
470        readbuffer = self.bytes_per_line
471
472        if self.format == scanext.FRAME_RGB: # "Color"
473            if self.depth == 8: # 8 bpp (32bit)
474                self.pad_bytes = self.bytes_per_line - 3 * self.pixels_per_line
475
476                log.debug("pad_bytes=%d" % self.pad_bytes)
477
478                dir = -1
479                if self.byte_format == 'RGBA':
480                    dir = 1
481
482                try:
483                    st, t = self.dev.readScan(readbuffer)
484                except scanext.error as stObj:
485                    st = stObj.args[0]
486                    self.updateQueue(st, 0)
487
488                while st == scanext.SANE_STATUS_GOOD:
489                    if t:
490                        len_t = len(t)
491                        w(b"".join([t[index:index+3:dir] + b'\xff' for index in range(0,len_t - self.pad_bytes,3)]))
492                        self.total_read += len_t
493                        self.total_write +=  len_t+(len_t - self.pad_bytes)/3
494                        self.updateQueue(st, self.total_read)
495                        log.debug("Color Read %d bytes" % self.total_read)
496
497                    else:
498                        time.sleep(0.1)
499
500                    try:
501                        st, t = self.dev.readScan(readbuffer)
502                    except scanext.error as stObj:
503                        st = stObj.args[0]
504                        self.updateQueue(st, self.total_read)
505                        break
506
507                    if self.checkCancel():
508                        break
509
510        elif self.format == scanext.FRAME_GRAY:
511
512            if self.depth == 1: # 1 bpp lineart
513                self.pad_bytes = self.bytes_per_line - (self.pixels_per_line + 7) // 8;
514
515                log.debug("pad_bytes=%d" % self.pad_bytes)
516
517                try:
518                    st, t = self.dev.readScan(readbuffer)
519                except scanext.error as stObj:
520                    st = stObj.args[0]
521                    self.updateQueue(st, 0)
522
523                while st == scanext.SANE_STATUS_GOOD:
524                    if t:
525                        len_t = len(t)
526                        w(b''.join([b''.join([b"\x00\x00\x00\xff" if k & ord(t[index:index+1]) else b"\xff\xff\xff\xff" for k in [0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1]]) for index in range(0, len_t - self.pad_bytes)]))
527                        self.total_read += len_t
528                        self.total_write += ((len_t - self.pad_bytes) * 32)
529                        self.updateQueue(st, self.total_read)
530                        log.debug("Lineart Read %d bytes" % self.total_read)
531                    else:
532                        time.sleep(0.1)
533
534                    try:
535                        st, t = self.dev.readScan(readbuffer)
536                    except scanext.error as stObj:
537                        st = stObj.args[0]
538                        self.updateQueue(st, self.total_read)
539                        break
540
541                    if self.checkCancel():
542                        break
543            elif self.depth == 8: # 8 bpp grayscale
544                self.pad_bytes = self.bytes_per_line - self.pixels_per_line
545
546                log.debug("pad_bytes=%d" % self.pad_bytes)
547                try:
548                    st, t = self.dev.readScan(readbuffer)
549                except scanext.error as stObj:
550                    st = stObj.args[0]
551                    self.updateQueue(st, 0)
552                while st == scanext.SANE_STATUS_GOOD:
553                    if t:
554                        len_t = len(t)
555                        w(b"".join([3*t[index:index+1] + b'\xff' for index in range(0, len_t - self.pad_bytes)]))
556                        self.total_read += len_t
557                        self.total_write += ((len_t  - self.pad_bytes) * 4)
558                        self.updateQueue(st, self.total_read)
559                        log.debug("Gray Read %d bytes" % self.total_read)
560                    else:
561                        time.sleep(0.1)
562
563                    try:
564                        st, t = self.dev.readScan(readbuffer)
565                    except scanext.error as stObj:
566                        st = stObj.args[0]
567                        self.updateQueue(st, self.total_read)
568                        break
569
570                    if self.checkCancel():
571                        break
572
573        #self.dev.cancelScan()
574        self.buffer.seek(0)
575        self.scan_active = False
576        log.debug("Scan thread exiting...")
577
578
579
580    def checkCancel(self):
581        canceled = False
582        while self.event_queue.qsize():
583            try:
584                event = self.event_queue.get(0)
585                if event == EVENT_SCAN_CANCELED:
586                    canceled = True
587                    log.debug("Cancel pressed!")
588                    self.dev.canclScan()
589
590
591            except queue.Empty:
592                break
593
594        return canceled
595
596
597
598def init():
599    return scanext.init()
600
601
602def deInit():
603    return scanext.deInit()
604
605
606def openDevice(dev):
607    "Open a device for scanning"
608    return ScanDevice(dev)
609
610
611def getDevices(local_only=0):
612    return scanext.getDevices(local_only)
613
614
615def reportError(code):
616    log.error("SANE: %s (code=%d)" % (scanext.getErrorMessage(code), code))
617
618
619