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          #---------------------------------------------------
    170 -        def __register_event_handlers(self):
    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          #---------------------------------------------------
    193 -        def _twain_handle_transfer_in_memory(self):
    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          #---------------------------------------------------
    216 -        def _twain_handle_transfer_by_file(self):
    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