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