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 init_result = _sane_module.init() 254 _log.info("SANE version: %s" % str(init_result)) 255 _log.debug('SANE device list: %s' % str(_sane_module.get_devices()))
256 #=======================================================
257 -class cSaneScanner:
258 259 # for testing uncomment "test" backend in /etc/sane/dll.conf 260 261 _src_manager = None 262
263 - def __init__(self, device=None):
264 _sane_import_module() 265 266 # FIXME: need to test against devs[x][0] 267 # devs = _sane_module.get_devices() 268 # if device not in devs: 269 # _log.error("device [%s] not found in list of devices detected by SANE" % device) 270 # _log.error(str(devs)) 271 # raise gmExceptions.ConstructorError, msg 272 273 self.__device = device 274 _log.info('using SANE device [%s]' % self.__device) 275 276 self.__init_scanner()
277 #---------------------------------------------------
278 - def __init_scanner(self):
279 self.__scanner = _sane_module.open(self.__device) 280 281 _log.debug('opened SANE device: %s' % str(self.__scanner)) 282 _log.debug('SANE device config: %s' % str(self.__scanner.get_parameters())) 283 _log.debug('SANE device opts : %s' % str(self.__scanner.optlist)) 284 _log.debug('SANE device opts : %s' % str(self.__scanner.get_options())) 285 286 return True
287 #---------------------------------------------------
288 - def close(self):
289 self.__scanner.close()
290 #---------------------------------------------------
291 - def acquire_pages_into_files(self, delay=None, filename=None):
292 if filename is None: 293 filename = gmTools.get_unique_filename(prefix='gmScannedObj-', suffix='.bmp') 294 else: 295 tmp, ext = os.path.splitext(filename) 296 if ext != '.bmp': 297 filename = filename + '.bmp' 298 299 filename = os.path.abspath(os.path.expanduser(filename)) 300 301 if delay is not None: 302 time.sleep(delay) 303 _log.debug('some sane backends report device_busy if we advance too fast. delay set to %s sec' % delay) 304 305 _log.debug('Trying to get image from scanner into [%s] !' % filename) 306 self.__scanner.start() 307 img = self.__scanner.snap() 308 img.save(filename) 309 310 return [filename]
311 #---------------------------------------------------
312 - def image_transfer_done(self):
313 return True
314 #--------------------------------------------------- 315 # def dummy(self): 316 # pass 317 # # supposedly there is a method *.close() but it does not 318 # # seem to work, therefore I put in the following line (else 319 # # it reports a busy sane-device on the second and consecutive runs) 320 # try: 321 # # by default use the first device 322 # # FIXME: room for improvement - option 323 # self.__scanner = _sane_module.open(_sane_module.get_devices()[0][0]) 324 # except Exception: 325 # _log.exception('cannot open SANE scanner') 326 # return False 327 # 328 # # Set scan parameters 329 # # FIXME: get those from config file 330 # #self.__scannercontrast=170 ; self.__scannerbrightness=150 ; self.__scannerwhite_level=190 331 # #self.__scannerdepth=6 332 # #self.__scannerbr_x = 412.0 333 # #self.__scannerbr_y = 583.0 334 335 #================================================== 336 # XSane handling 337 #==================================================
338 -class cXSaneScanner:
339 340 _FILETYPE = '.png' 341 342 #----------------------------------------------
343 - def __init__(self):
344 # while not strictly necessary it is good to fail early 345 # this will tell us fairly safely whether XSane is properly installed 346 self._stock_xsanerc = os.path.expanduser(os.path.join('~', '.sane', 'xsane', 'xsane.rc')) 347 try: 348 open(self._stock_xsanerc, 'r').close() 349 except IOError: 350 msg = ( 351 'XSane not properly installed for this user:\n\n' 352 ' [%s] not found\n\n' 353 'Start XSane once before using it with GNUmed.' 354 ) % self._stock_xsanerc 355 raise ImportError(msg) 356 357 # make sure we've got a custom xsanerc for 358 # the user to modify manually 359 self._gm_custom_xsanerc = os.path.expanduser(os.path.join('~', '.gnumed', 'gm-xsanerc.conf')) 360 try: 361 open(self._gm_custom_xsanerc, 'r+b').close() 362 except IOError: 363 _log.info('creating [%s] from [%s]', self._gm_custom_xsanerc, self._stock_xsanerc) 364 shutil.copyfile(self._stock_xsanerc, self._gm_custom_xsanerc) 365 366 self.device_settings_file = None 367 self.default_device = None
368 #----------------------------------------------
369 - def close(self):
370 pass
371 #----------------------------------------------
372 - def acquire_pages_into_files(self, delay=None, filename=None):
373 """Call XSane. 374 375 <filename> name part must have format name-001.ext> 376 """ 377 if filename is None: 378 filename = gmTools.get_unique_filename(prefix = 'gm-scan-') 379 380 name, ext = os.path.splitext(filename) 381 filename = '%s-001%s' % (name, cXSaneScanner._FILETYPE) 382 filename = os.path.abspath(os.path.expanduser(filename)) 383 384 cmd = 'xsane --no-mode-selection --save --force-filename "%s" --xsane-rc "%s" %s %s' % ( 385 filename, 386 self.__get_session_xsanerc(), 387 gmTools.coalesce(self.device_settings_file, '', '--device-settings %s'), 388 gmTools.coalesce(self.default_device, '') 389 ) 390 normal_exit = gmShellAPI.run_command_in_shell(command = cmd, blocking = True) 391 392 if normal_exit: 393 flist = glob.glob(filename.replace('001', '*')) 394 flist.sort() 395 return flist 396 397 raise OSError(-1, 'error running XSane as [%s]' % cmd)
398 #---------------------------------------------------
399 - def image_transfer_done(self):
400 return True
401 #---------------------------------------------- 402 # internal API 403 #----------------------------------------------
404 - def __get_session_xsanerc(self):
405 406 # create an xsanerc for this session 407 session_xsanerc = gmTools.get_unique_filename ( 408 prefix = 'gm-session_xsanerc-', 409 suffix = '.conf' 410 ) 411 _log.debug('GNUmed -> XSane session xsanerc: %s', session_xsanerc) 412 413 # our closest bet, might contain umlauts 414 enc = gmI18N.get_encoding() 415 fread = io.open(self._gm_custom_xsanerc, mode = "rt", encoding = enc) 416 fwrite = io.open(session_xsanerc, mode = "wt", encoding = enc) 417 418 paths = gmTools.gmPaths() 419 val_dict = { 420 'tmp-path': paths.tmp_dir, 421 'working-directory': paths.tmp_dir, 422 'filename': '<--force-filename>', 423 'filetype': cXSaneScanner._FILETYPE, 424 'skip-existing-numbers': '1', 425 'filename-counter-step': '1', 426 'filename-counter-len': '3' 427 } 428 429 for idx, line in enumerate(fread): 430 line = line.replace('\n', '') 431 line = line.replace('\r', '') 432 433 if idx % 2 == 0: # even lines are keys 434 curr_key = line.strip('"') 435 fwrite.write('"%s"\n' % curr_key) 436 else: # odd lines are corresponding values 437 try: 438 value = val_dict[curr_key] 439 _log.debug('replaced [%s] with [%s]', curr_key, val_dict[curr_key]) 440 except KeyError: 441 value = line 442 fwrite.write('%s\n' % value) 443 444 fwrite.flush() 445 fwrite.close() 446 fread.close() 447 448 return session_xsanerc
449 #==================================================
450 -def get_devices():
451 try: 452 _twain_import_module() 453 # TWAIN does not support get_devices(): 454 # devices can only be selected from within TWAIN itself 455 return None 456 except ImportError: 457 pass 458 459 if use_XSane: 460 # neither does XSane 461 return None 462 463 _sane_import_module() 464 return _sane_module.get_devices()
465 #-----------------------------------------------------
466 -def acquire_pages_into_files(device=None, delay=None, filename=None, calling_window=None, xsane_device_settings=None):
467 """Connect to a scanner and return the scanned pages as a file list. 468 469 returns: 470 - list of filenames: names of scanned pages, may be [] 471 - None: unable to connect to scanner 472 """ 473 try: 474 scanner = cTwainScanner(calling_window=calling_window) 475 _log.debug('using TWAIN') 476 except ImportError: 477 if use_XSane: 478 _log.debug('using XSane') 479 scanner = cXSaneScanner() 480 scanner.device_settings_file = xsane_device_settings 481 scanner.default_device = device 482 else: 483 _log.debug('using SANE directly') 484 scanner = cSaneScanner(device=device) 485 486 _log.debug('requested filename: [%s]' % filename) 487 fnames = scanner.acquire_pages_into_files(filename=filename, delay=delay) 488 scanner.close() 489 _log.debug('acquired pages into files: %s' % str(fnames)) 490 491 return fnames
492 #================================================== 493 # main 494 #================================================== 495 if __name__ == '__main__': 496 497 if len(sys.argv) > 1 and sys.argv[1] == 'test': 498 499 logging.basicConfig(level=logging.DEBUG) 500 501 print("devices:") 502 print(get_devices()) 503 504 sys.exit() 505 506 setups = [ 507 {'dev': 'test:0', 'file': 'x1-test0-1-0001'}, 508 {'dev': 'test:1', 'file': 'x2-test1-1-0001.bmp'}, 509 {'dev': 'test:0', 'file': 'x3-test0-2-0001.bmp-ccc'} 510 ] 511 512 idx = 1 513 for setup in setups: 514 print("scanning page #%s from device [%s]" % (idx, setup['dev'])) 515 idx += 1 516 fnames = acquire_pages_into_files(device = setup['dev'], filename = setup['file'], delay = (idx*5)) 517 if fnames is False: 518 print("error, cannot acquire page") 519 else: 520 print(" image files:", fnames) 521