Gnumed.pycommon.gmScanBackend

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