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          #---------------------------------------------------
    172 -        def __register_event_handlers(self):
    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          #---------------------------------------------------
    195 -        def _twain_handle_transfer_in_memory(self):
    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          #---------------------------------------------------
    218 -        def _twain_handle_transfer_by_file(self):
    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