| Home | Trees | Indices | Help |
|
|---|
|
|
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 #=======================================================
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 #=======================================================
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
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 #---------------------------------------------------
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 #---------------------------------------------------
90 #---------------------------------------------------
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 #---------------------------------------------------
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 #---------------------------------------------------
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 #---------------------------------------------------
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 #---------------------------------------------------
190 _log.info("being asked to close data source")
191 #---------------------------------------------------
193 _log.info("being asked to save application state")
194 #---------------------------------------------------
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 #=======================================================
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 #=======================================================
262
263 # for testing uncomment "test" backend in /etc/sane/dll.conf
264
265 _src_manager = None
266
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 #---------------------------------------------------
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 #---------------------------------------------------
293 self.__scanner.close()
294 #---------------------------------------------------
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 #---------------------------------------------------
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 #==================================================
343
344 _FILETYPE = '.png'
345
346 #----------------------------------------------
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 #----------------------------------------------
375 #----------------------------------------------
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 #---------------------------------------------------
405 #----------------------------------------------
406 # internal API
407 #----------------------------------------------
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 #==================================================
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Jan 25 02:55:27 2019 | http://epydoc.sourceforge.net |