Package Gnumed :: Package pycommon :: Module gmScanBackend
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmScanBackend

  1  #================================================== 
  2  # GNUmed SANE/TWAIN scanner classes 
  3  #================================================== 
  4   
  5   
  6   
  7  __license__ = "GPL v2 or later" 
  8  __author__ = """Sebastian Hilbert <Sebastian.Hilbert@gmx.net>, Karsten Hilbert <Karsten.Hilbert@gmx.net>""" 
  9   
 10   
 11  # stdlib 
 12  import sys 
 13  import os.path 
 14  import os 
 15  import time 
 16  import shutil 
 17  import io 
 18  import glob 
 19  import logging 
 20  #import stat 
 21   
 22   
 23  # GNUmed 
 24  if __name__ == '__main__': 
 25          sys.path.insert(0, '../../') 
 26  from Gnumed.pycommon import gmShellAPI 
 27  from Gnumed.pycommon import gmTools 
 28  from Gnumed.pycommon import gmI18N 
 29  from Gnumed.pycommon import gmLog2 
 30   
 31   
 32  _log = logging.getLogger('gm.scanning') 
 33   
 34  _twain_module = None 
 35  _sane_module = None 
 36   
 37  use_XSane = True 
 38  #======================================================= 
 39  # TWAIN handling 
 40  #======================================================= 
41 -def _twain_import_module():
42 global _twain_module 43 if _twain_module is None: 44 try: 45 import twain 46 _twain_module = twain 47 except ImportError: 48 _log.exception('cannot import TWAIN module (WinTWAIN.py)') 49 raise 50 _log.info("TWAIN version: %s" % _twain_module.Version())
51 #=======================================================
52 -class cTwainScanner:
53 54 # http://twainmodule.sourceforge.net/docs/index.html 55 56 # FIXME: we need to handle this exception in the right place: <class 'twain.excTWCC_SUCCESS'> 57
58 - def __init__(self, calling_window=None):
59 _twain_import_module() 60 61 self.__calling_window = calling_window 62 self.__src_manager = None 63 self.__scanner = None 64 self.__done_transferring_image = False 65 66 self.__register_event_handlers()
67 #--------------------------------------------------- 68 # external API 69 #---------------------------------------------------
70 - def acquire_pages_into_files(self, delay=None, filename=None):
71 if filename is None: 72 filename = gmTools.get_unique_filename(prefix = 'gmScannedObj-', suffix = '.bmp') 73 else: 74 tmp, ext = os.path.splitext(filename) 75 if ext != '.bmp': 76 filename = filename + '.bmp' 77 78 self.__filename = os.path.abspath(os.path.expanduser(filename)) 79 80 if not self.__init_scanner(): 81 raise OSError(-1, 'cannot init TWAIN scanner device') 82 83 self.__done_transferring_image = False 84 self.__scanner.RequestAcquire(True) 85 86 return [self.__filename]
87 #---------------------------------------------------
88 - def image_transfer_done(self):
89 return self.__done_transferring_image
90 #---------------------------------------------------
91 - def close(self):
92 # close() is called after acquire_pages*() so if we destroy the source 93 # before TWAIN is done we hang it, an RequestAcquire() only *requests* 94 # a scan, we would have to wait for process_xfer to finisch before 95 # destroying the source, and even then it might destroy state in the 96 # non-Python TWAIN subsystem 97 #********************************** 98 # if we do this TWAIN does not work 99 #********************************** 100 # if self.__scanner is not None: 101 # self.__scanner.destroy() 102 103 # if self.__src_manager is not None: 104 # self.__src_manager.destroy() 105 106 # del self.__scanner 107 # del self.__src_manager 108 return
109 #--------------------------------------------------- 110 # internal helpers 111 #---------------------------------------------------
112 - def __init_scanner(self):
113 if self.__scanner is not None: 114 return True 115 116 self.__init_src_manager() 117 if self.__src_manager is None: 118 return False 119 120 # TWAIN will notify us when the image is scanned 121 self.__src_manager.SetCallback(self._twain_event_callback) 122 123 # no arg == show "select source" dialog 124 try: 125 self.__scanner = self.__src_manager.OpenSource() 126 except _twain_module.excDSOpenFailed: 127 _log.exception('cannot open TWAIN data source (image capture device)') 128 gmLog2.log_stack_trace() 129 return False 130 131 if self.__scanner is None: 132 _log.error("user canceled scan source selection dialog") 133 return False 134 135 _log.info("TWAIN data source: %s" % self.__scanner.GetSourceName()) 136 _log.debug("TWAIN data source config: %s" % str(self.__scanner.GetIdentity())) 137 138 return True
139 #---------------------------------------------------
140 - def __init_src_manager(self):
141 142 if self.__src_manager is not None: 143 return 144 145 # clean up scanner driver since we will initialize the source manager 146 # if self.__scanner is not None: 147 # self.__scanner.destroy() # this probably should not be done here 148 # del self.__scanner # try to sneak this back in later 149 # self.__scanner = None # this really should work 150 151 # TWAIN talks to us via MS-Windows message queues 152 # so we need to pass it a handle to ourselves, 153 # the following fails with "attempt to create Pseudo Window failed", 154 # I assume because the TWAIN vendors want to sabotage rebranding their GUI 155 # self.__src_manager = _twain_module.SourceManager(self.__calling_window.GetHandle(), ProductName = 'GNUmed - The EMR that never sleeps.') 156 try: 157 self.__src_manager = _twain_module.SourceManager(self.__calling_window.GetHandle()) 158 159 except _twain_module.excSMLoadFileFailed: 160 _log.exception('failed to load TWAIN_32.DLL') 161 return 162 163 except _twain_module.excSMGetProcAddressFailed: 164 _log.exception('failed to jump into TWAIN_32.DLL') 165 return 166 167 except _twain_module.excSMOpenFailed: 168 _log.exception('failed to open Source Manager') 169 return 170 171 _log.info("TWAIN source manager config: %s" % str(self.__src_manager.GetIdentity()))
172 #--------------------------------------------------- 173 # TWAIN callback handling 174 #---------------------------------------------------
176 self.__twain_event_handlers = { 177 _twain_module.MSG_XFERREADY: self._twain_handle_transfer_in_memory, 178 _twain_module.MSG_CLOSEDSREQ: self._twain_close_datasource, 179 _twain_module.MSG_CLOSEDSOK: self._twain_save_state, 180 _twain_module.MSG_DEVICEEVENT: self._twain_handle_src_event 181 }
182 #---------------------------------------------------
183 - def _twain_event_callback(self, twain_event):
184 _log.debug('notification of TWAIN event <%s>' % str(twain_event)) 185 self.__twain_event_handlers[twain_event]() 186 self.__scanner = None 187 return
188 #---------------------------------------------------
189 - def _twain_close_datasource(self):
190 _log.info("being asked to close data source")
191 #---------------------------------------------------
192 - def _twain_save_state(self):
193 _log.info("being asked to save application state")
194 #---------------------------------------------------
195 - def _twain_handle_src_event(self):
196 _log.info("being asked to handle device specific event")
197 #---------------------------------------------------
199 200 # FIXME: handle several images 201 202 _log.debug('receiving image from TWAIN source') 203 _log.debug('image info: %s' % self.__scanner.GetImageInfo()) 204 _log.debug('image layout: %s' % str(self.__scanner.GetImageLayout())) 205 206 # get image from source 207 (external_data_handle, more_images_pending) = self.__scanner.XferImageNatively() 208 try: 209 # convert DIB to standard bitmap file (always .bmp) 210 _twain_module.DIBToBMFile(external_data_handle, self.__filename) 211 finally: 212 _twain_module.GlobalHandleFree(external_data_handle) 213 _log.debug('%s pending images' % more_images_pending) 214 215 # hide the scanner user interface again 216 # self.__scanner.HideUI() # needed ? 217 # self.__scanner = None # not sure why this should be needed, simple_wx does it, though 218 219 self.__done_transferring_image = True
220 #---------------------------------------------------
222 223 # the docs say this is not required to be implemented 224 # therefor we can't use it by default :-( 225 # UNTESTED !!!! 226 227 _log.debug('receiving image from TWAIN source') 228 _log.debug('image info: %s' % self.__scanner.GetImageInfo()) 229 _log.debug('image layout: %s' % self.__scanner.GetImageLayout()) 230 231 self.__scanner.SetXferFileName(self.__filename) # FIXME: allow format 232 233 more_images_pending = self.__scanner.XferImageByFile() 234 _log.debug('%s pending images' % more_images_pending) 235 236 # hide the scanner user interface again 237 self.__scanner.HideUI() 238 # self.__scanner = None 239 240 return
241 #======================================================= 242 # SANE handling 243 #=======================================================
244 -def _sane_import_module():
245 global _sane_module 246 if _sane_module is None: 247 try: 248 import sane 249 except ImportError: 250 _log.exception('cannot import SANE module') 251 raise 252 _sane_module = sane 253 try: 254 init_result = _sane_module.init() 255 except: 256 _log.exception('cannot init SANE module') 257 raise 258 _log.info("SANE version: %s" % str(init_result)) 259 _log.debug('SANE device list: %s' % str(_sane_module.get_devices()))
260 #=======================================================
261 -class cSaneScanner:
262 263 # for testing uncomment "test" backend in /etc/sane/dll.conf 264 265 _src_manager = None 266
267 - def __init__(self, device=None):
268 _sane_import_module() 269 270 # FIXME: need to test against devs[x][0] 271 # devs = _sane_module.get_devices() 272 # if device not in devs: 273 # _log.error("device [%s] not found in list of devices detected by SANE" % device) 274 # _log.error(str(devs)) 275 # raise gmExceptions.ConstructorError, msg 276 277 self.__device = device 278 _log.info('using SANE device [%s]' % self.__device) 279 280 self.__init_scanner()
281 #---------------------------------------------------
282 - def __init_scanner(self):
283 self.__scanner = _sane_module.open(self.__device) 284 285 _log.debug('opened SANE device: %s' % str(self.__scanner)) 286 _log.debug('SANE device config: %s' % str(self.__scanner.get_parameters())) 287 _log.debug('SANE device opts : %s' % str(self.__scanner.optlist)) 288 _log.debug('SANE device opts : %s' % str(self.__scanner.get_options())) 289 290 return True
291 #---------------------------------------------------
292 - def close(self):
293 self.__scanner.close()
294 #---------------------------------------------------
295 - def acquire_pages_into_files(self, delay=None, filename=None):
296 if filename is None: 297 filename = gmTools.get_unique_filename(prefix='gmScannedObj-', suffix='.bmp') 298 else: 299 tmp, ext = os.path.splitext(filename) 300 if ext != '.bmp': 301 filename = filename + '.bmp' 302 303 filename = os.path.abspath(os.path.expanduser(filename)) 304 305 if delay is not None: 306 time.sleep(delay) 307 _log.debug('some sane backends report device_busy if we advance too fast. delay set to %s sec' % delay) 308 309 _log.debug('Trying to get image from scanner into [%s] !' % filename) 310 self.__scanner.start() 311 img = self.__scanner.snap() 312 img.save(filename) 313 314 return [filename]
315 #---------------------------------------------------
316 - def image_transfer_done(self):
317 return True
318 #--------------------------------------------------- 319 # def dummy(self): 320 # pass 321 # # supposedly there is a method *.close() but it does not 322 # # seem to work, therefore I put in the following line (else 323 # # it reports a busy sane-device on the second and consecutive runs) 324 # try: 325 # # by default use the first device 326 # # FIXME: room for improvement - option 327 # self.__scanner = _sane_module.open(_sane_module.get_devices()[0][0]) 328 # except: 329 # _log.exception('cannot open SANE scanner') 330 # return False 331 # 332 # # Set scan parameters 333 # # FIXME: get those from config file 334 # #self.__scannercontrast=170 ; self.__scannerbrightness=150 ; self.__scannerwhite_level=190 335 # #self.__scannerdepth=6 336 # #self.__scannerbr_x = 412.0 337 # #self.__scannerbr_y = 583.0 338 339 #================================================== 340 # XSane handling 341 #==================================================
342 -class cXSaneScanner:
343 344 _FILETYPE = '.png' 345 346 #----------------------------------------------
347 - def __init__(self):
348 # while not strictly necessary it is good to fail early 349 # this will tell us fairly safely whether XSane is properly installed 350 self._stock_xsanerc = os.path.expanduser(os.path.join('~', '.sane', 'xsane', 'xsane.rc')) 351 try: 352 open(self._stock_xsanerc, 'r').close() 353 except IOError: 354 msg = ( 355 'XSane not properly installed for this user:\n\n' 356 ' [%s] not found\n\n' 357 'Start XSane once before using it with GNUmed.' 358 ) % self._stock_xsanerc 359 raise ImportError(msg) 360 361 # make sure we've got a custom xsanerc for 362 # the user to modify manually 363 self._gm_custom_xsanerc = os.path.expanduser(os.path.join('~', '.gnumed', 'gm-xsanerc.conf')) 364 try: 365 open(self._gm_custom_xsanerc, 'r+b').close() 366 except IOError: 367 _log.info('creating [%s] from [%s]', self._gm_custom_xsanerc, self._stock_xsanerc) 368 shutil.copyfile(self._stock_xsanerc, self._gm_custom_xsanerc) 369 370 self.device_settings_file = None 371 self.default_device = None
372 #----------------------------------------------
373 - def close(self):
374 pass
375 #----------------------------------------------
376 - def acquire_pages_into_files(self, delay=None, filename=None):
377 """Call XSane. 378 379 <filename> name part must have format name-001.ext> 380 """ 381 if filename is None: 382 filename = gmTools.get_unique_filename(prefix = 'gm-scan-') 383 384 name, ext = os.path.splitext(filename) 385 filename = '%s-001%s' % (name, cXSaneScanner._FILETYPE) 386 filename = os.path.abspath(os.path.expanduser(filename)) 387 388 cmd = 'xsane --no-mode-selection --save --force-filename "%s" --xsane-rc "%s" %s %s' % ( 389 filename, 390 self.__get_session_xsanerc(), 391 gmTools.coalesce(self.device_settings_file, '', '--device-settings %s'), 392 gmTools.coalesce(self.default_device, '') 393 ) 394 normal_exit = gmShellAPI.run_command_in_shell(command = cmd, blocking = True) 395 396 if normal_exit: 397 flist = glob.glob(filename.replace('001', '*')) 398 flist.sort() 399 return flist 400 401 raise OSError(-1, 'error running XSane as [%s]' % cmd)
402 #---------------------------------------------------
403 - def image_transfer_done(self):
404 return True
405 #---------------------------------------------- 406 # internal API 407 #----------------------------------------------
408 - def __get_session_xsanerc(self):
409 410 # create an xsanerc for this session 411 session_xsanerc = gmTools.get_unique_filename ( 412 prefix = 'gm-session_xsanerc-', 413 suffix = '.conf' 414 ) 415 _log.debug('GNUmed -> XSane session xsanerc: %s', session_xsanerc) 416 417 # our closest bet, might contain umlauts 418 enc = gmI18N.get_encoding() 419 fread = io.open(self._gm_custom_xsanerc, mode = "rt", encoding = enc) 420 fwrite = io.open(session_xsanerc, mode = "wt", encoding = enc) 421 422 paths = gmTools.gmPaths() 423 val_dict = { 424 'tmp-path': paths.tmp_dir, 425 'working-directory': paths.tmp_dir, 426 'filename': '<--force-filename>', 427 'filetype': cXSaneScanner._FILETYPE, 428 'skip-existing-numbers': '1', 429 'filename-counter-step': '1', 430 'filename-counter-len': '3' 431 } 432 433 for idx, line in enumerate(fread): 434 line = line.replace('\n', '') 435 line = line.replace('\r', '') 436 437 if idx % 2 == 0: # even lines are keys 438 curr_key = line.strip('"') 439 fwrite.write('"%s"\n' % curr_key) 440 else: # odd lines are corresponding values 441 try: 442 value = val_dict[curr_key] 443 _log.debug('replaced [%s] with [%s]', curr_key, val_dict[curr_key]) 444 except KeyError: 445 value = line 446 fwrite.write('%s\n' % value) 447 448 fwrite.flush() 449 fwrite.close() 450 fread.close() 451 452 return session_xsanerc
453 #==================================================
454 -def get_devices():
455 try: 456 _twain_import_module() 457 # TWAIN does not support get_devices(): 458 # devices can only be selected from within TWAIN itself 459 return None 460 except ImportError: 461 pass 462 463 if use_XSane: 464 # neither does XSane 465 return None 466 467 _sane_import_module() 468 return _sane_module.get_devices()
469 #-----------------------------------------------------
470 -def acquire_pages_into_files(device=None, delay=None, filename=None, calling_window=None, xsane_device_settings=None):
471 """Connect to a scanner and return the scanned pages as a file list. 472 473 returns: 474 - list of filenames: names of scanned pages, may be [] 475 - None: unable to connect to scanner 476 """ 477 try: 478 scanner = cTwainScanner(calling_window=calling_window) 479 _log.debug('using TWAIN') 480 except ImportError: 481 if use_XSane: 482 _log.debug('using XSane') 483 scanner = cXSaneScanner() 484 scanner.device_settings_file = xsane_device_settings 485 scanner.default_device = device 486 else: 487 _log.debug('using SANE directly') 488 scanner = cSaneScanner(device=device) 489 490 _log.debug('requested filename: [%s]' % filename) 491 fnames = scanner.acquire_pages_into_files(filename=filename, delay=delay) 492 scanner.close() 493 _log.debug('acquired pages into files: %s' % str(fnames)) 494 495 return fnames
496 #================================================== 497 # main 498 #================================================== 499 if __name__ == '__main__': 500 501 if len(sys.argv) > 1 and sys.argv[1] == 'test': 502 503 logging.basicConfig(level=logging.DEBUG) 504 505 print("devices:") 506 print(get_devices()) 507 508 sys.exit() 509 510 setups = [ 511 {'dev': 'test:0', 'file': 'x1-test0-1-0001'}, 512 {'dev': 'test:1', 'file': 'x2-test1-1-0001.bmp'}, 513 {'dev': 'test:0', 'file': 'x3-test0-2-0001.bmp-ccc'} 514 ] 515 516 idx = 1 517 for setup in setups: 518 print("scanning page #%s from device [%s]" % (idx, setup['dev'])) 519 idx += 1 520 fnames = acquire_pages_into_files(device = setup['dev'], filename = setup['file'], delay = (idx*5)) 521 if fnames is False: 522 print("error, cannot acquire page") 523 else: 524 print(" image files:", fnames) 525