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