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