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