2010-07-31 06:52:47 +04:00
|
|
|
# sane.py
|
|
|
|
#
|
|
|
|
# Python wrapper on top of the _sane module, which is in turn a very
|
|
|
|
# thin wrapper on top of the SANE library. For a complete understanding
|
|
|
|
# of SANE, consult the documentation at the SANE home page:
|
|
|
|
# http://www.mostang.com/sane/ .
|
|
|
|
|
|
|
|
__version__ = '2.0'
|
|
|
|
__author__ = ['Andrew Kuchling', 'Ralph Heinkel']
|
|
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
import _sane
|
|
|
|
from _sane import *
|
|
|
|
|
|
|
|
TYPE_STR = { TYPE_BOOL: "TYPE_BOOL", TYPE_INT: "TYPE_INT",
|
|
|
|
TYPE_FIXED: "TYPE_FIXED", TYPE_STRING: "TYPE_STRING",
|
|
|
|
TYPE_BUTTON: "TYPE_BUTTON", TYPE_GROUP: "TYPE_GROUP" }
|
|
|
|
|
|
|
|
UNIT_STR = { UNIT_NONE: "UNIT_NONE",
|
|
|
|
UNIT_PIXEL: "UNIT_PIXEL",
|
|
|
|
UNIT_BIT: "UNIT_BIT",
|
|
|
|
UNIT_MM: "UNIT_MM",
|
|
|
|
UNIT_DPI: "UNIT_DPI",
|
|
|
|
UNIT_PERCENT: "UNIT_PERCENT",
|
|
|
|
UNIT_MICROSECOND: "UNIT_MICROSECOND" }
|
|
|
|
|
|
|
|
|
|
|
|
class Option:
|
|
|
|
"""Class representing a SANE option.
|
|
|
|
Attributes:
|
|
|
|
index -- number from 0 to n, giving the option number
|
|
|
|
name -- a string uniquely identifying the option
|
|
|
|
title -- single-line string containing a title for the option
|
|
|
|
desc -- a long string describing the option; useful as a help message
|
|
|
|
type -- type of this option. Possible values: TYPE_BOOL,
|
|
|
|
TYPE_INT, TYPE_STRING, and so forth.
|
|
|
|
unit -- units of this option. Possible values: UNIT_NONE,
|
|
|
|
UNIT_PIXEL, etc.
|
|
|
|
size -- size of the value in bytes
|
|
|
|
cap -- capabilities available; CAP_EMULATED, CAP_SOFT_SELECT, etc.
|
|
|
|
constraint -- constraint on values. Possible values:
|
|
|
|
None : No constraint
|
|
|
|
(min,max,step) Integer values, from min to max, stepping by
|
|
|
|
list of integers or strings: only the listed values are allowed
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, args, scanDev):
|
|
|
|
self.scanDev = scanDev # needed to get current value of this option
|
|
|
|
self.index, self.name = args[0], args[1]
|
|
|
|
self.title, self.desc = args[2], args[3]
|
|
|
|
self.type, self.unit = args[4], args[5]
|
|
|
|
self.size, self.cap = args[6], args[7]
|
|
|
|
self.constraint = args[8]
|
|
|
|
def f(x):
|
|
|
|
if x=='-': return '_'
|
|
|
|
else: return x
|
|
|
|
if type(self.name)!=type(''): self.py_name=str(self.name)
|
2012-10-11 02:11:13 +04:00
|
|
|
else: self.py_name=''.join(map(f, self.name))
|
2010-07-31 06:52:47 +04:00
|
|
|
|
|
|
|
def is_active(self):
|
|
|
|
return _sane.OPTION_IS_ACTIVE(self.cap)
|
|
|
|
def is_settable(self):
|
|
|
|
return _sane.OPTION_IS_SETTABLE(self.cap)
|
|
|
|
def __repr__(self):
|
|
|
|
if self.is_settable():
|
|
|
|
settable = 'yes'
|
|
|
|
else:
|
|
|
|
settable = 'no'
|
|
|
|
if self.is_active():
|
|
|
|
active = 'yes'
|
|
|
|
curValue = repr(getattr(self.scanDev, self.py_name))
|
|
|
|
else:
|
|
|
|
active = 'no'
|
|
|
|
curValue = '<not available, inactive option>'
|
|
|
|
s = """\nName: %s
|
|
|
|
Cur value: %s
|
|
|
|
Index: %d
|
|
|
|
Title: %s
|
|
|
|
Desc: %s
|
|
|
|
Type: %s
|
|
|
|
Unit: %s
|
|
|
|
Constr: %s
|
|
|
|
active: %s
|
|
|
|
settable: %s\n""" % (self.py_name, curValue,
|
|
|
|
self.index, self.title, self.desc,
|
|
|
|
TYPE_STR[self.type], UNIT_STR[self.unit],
|
|
|
|
`self.constraint`, active, settable)
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
class _SaneIterator:
|
|
|
|
""" intended for ADF scans.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, device):
|
|
|
|
self.device = device
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
self.device.cancel()
|
|
|
|
|
|
|
|
def next(self):
|
|
|
|
try:
|
|
|
|
self.device.start()
|
|
|
|
except error, v:
|
|
|
|
if v == 'Document feeder out of documents':
|
|
|
|
raise StopIteration
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
return self.device.snap(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SaneDev:
|
|
|
|
"""Class representing a SANE device.
|
|
|
|
Methods:
|
|
|
|
start() -- initiate a scan, using the current settings
|
|
|
|
snap() -- snap a picture, returning an Image object
|
|
|
|
arr_snap() -- snap a picture, returning a numarray object
|
|
|
|
cancel() -- cancel an in-progress scanning operation
|
|
|
|
fileno() -- return the file descriptor for the scanner (handy for select)
|
|
|
|
|
|
|
|
Also available, but rather low-level:
|
|
|
|
get_parameters() -- get the current parameter settings of the device
|
|
|
|
get_options() -- return a list of tuples describing all the options.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
optlist -- list of option names
|
|
|
|
|
|
|
|
You can also access an option name to retrieve its value, and to
|
|
|
|
set it. For example, if one option has a .name attribute of
|
|
|
|
imagemode, and scanner is a SaneDev object, you can do:
|
|
|
|
print scanner.imagemode
|
|
|
|
scanner.imagemode = 'Full frame'
|
|
|
|
scanner.['imagemode'] returns the corresponding Option object.
|
|
|
|
"""
|
|
|
|
def __init__(self, devname):
|
|
|
|
d=self.__dict__
|
|
|
|
d['sane_signature'] = self._getSaneSignature(devname)
|
|
|
|
d['scanner_model'] = d['sane_signature'][1:3]
|
|
|
|
d['dev'] = _sane._open(devname)
|
|
|
|
self.__load_option_dict()
|
|
|
|
|
|
|
|
def _getSaneSignature(self, devname):
|
|
|
|
devices = get_devices()
|
|
|
|
if not devices:
|
|
|
|
raise RuntimeError('no scanner available')
|
|
|
|
for dev in devices:
|
|
|
|
if devname == dev[0]:
|
|
|
|
return dev
|
|
|
|
raise RuntimeError('no such scan device "%s"' % devname)
|
|
|
|
|
|
|
|
def __load_option_dict(self):
|
|
|
|
d=self.__dict__
|
|
|
|
d['opt']={}
|
|
|
|
optlist=d['dev'].get_options()
|
|
|
|
for t in optlist:
|
|
|
|
o=Option(t, self)
|
|
|
|
if o.type!=TYPE_GROUP:
|
|
|
|
d['opt'][o.py_name]=o
|
|
|
|
|
|
|
|
def __setattr__(self, key, value):
|
|
|
|
dev=self.__dict__['dev']
|
|
|
|
optdict=self.__dict__['opt']
|
|
|
|
if not optdict.has_key(key):
|
|
|
|
self.__dict__[key]=value ; return
|
|
|
|
opt=optdict[key]
|
|
|
|
if opt.type==TYPE_GROUP:
|
|
|
|
raise AttributeError, "Groups can't be set: "+key
|
|
|
|
if not _sane.OPTION_IS_ACTIVE(opt.cap):
|
|
|
|
raise AttributeError, 'Inactive option: '+key
|
|
|
|
if not _sane.OPTION_IS_SETTABLE(opt.cap):
|
|
|
|
raise AttributeError, "Option can't be set by software: "+key
|
|
|
|
if type(value) == int and opt.type == TYPE_FIXED:
|
|
|
|
# avoid annoying errors of backend if int is given instead float:
|
|
|
|
value = float(value)
|
|
|
|
self.last_opt = dev.set_option(opt.index, value)
|
|
|
|
# do binary AND to find if we have to reload options:
|
|
|
|
if self.last_opt & INFO_RELOAD_OPTIONS:
|
|
|
|
self.__load_option_dict()
|
|
|
|
|
|
|
|
def __getattr__(self, key):
|
|
|
|
dev=self.__dict__['dev']
|
|
|
|
optdict=self.__dict__['opt']
|
|
|
|
if key=='optlist':
|
|
|
|
return self.opt.keys()
|
|
|
|
if key=='area':
|
|
|
|
return (self.tl_x, self.tl_y),(self.br_x, self.br_y)
|
|
|
|
if not optdict.has_key(key):
|
|
|
|
raise AttributeError, 'No such attribute: '+key
|
|
|
|
opt=optdict[key]
|
|
|
|
if opt.type==TYPE_BUTTON:
|
|
|
|
raise AttributeError, "Buttons don't have values: "+key
|
|
|
|
if opt.type==TYPE_GROUP:
|
|
|
|
raise AttributeError, "Groups don't have values: "+key
|
|
|
|
if not _sane.OPTION_IS_ACTIVE(opt.cap):
|
|
|
|
raise AttributeError, 'Inactive option: '+key
|
|
|
|
value = dev.get_option(opt.index)
|
|
|
|
return value
|
|
|
|
|
|
|
|
def __getitem__(self, key):
|
|
|
|
return self.opt[key]
|
|
|
|
|
|
|
|
def get_parameters(self):
|
|
|
|
"""Return a 5-tuple holding all the current device settings:
|
|
|
|
(format, last_frame, (pixels_per_line, lines), depth, bytes_per_line)
|
|
|
|
|
|
|
|
- format is one of 'L' (grey), 'RGB', 'R' (red), 'G' (green), 'B' (blue).
|
|
|
|
- last_frame [bool] indicates if this is the last frame of a multi frame image
|
|
|
|
- (pixels_per_line, lines) specifies the size of the scanned image (x,y)
|
|
|
|
- lines denotes the number of scanlines per frame
|
|
|
|
- depth gives number of pixels per sample
|
|
|
|
"""
|
|
|
|
return self.dev.get_parameters()
|
|
|
|
|
|
|
|
def get_options(self):
|
|
|
|
"Return a list of tuples describing all the available options"
|
|
|
|
return self.dev.get_options()
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
"Initiate a scanning operation"
|
|
|
|
return self.dev.start()
|
|
|
|
|
|
|
|
def cancel(self):
|
|
|
|
"Cancel an in-progress scanning operation"
|
|
|
|
return self.dev.cancel()
|
|
|
|
|
|
|
|
def snap(self, no_cancel=0):
|
|
|
|
"Snap a picture, returning a PIL image object with the results"
|
|
|
|
(mode, last_frame,
|
|
|
|
(xsize, ysize), depth, bytes_per_line) = self.get_parameters()
|
|
|
|
if mode in ['gray', 'red', 'green', 'blue']:
|
|
|
|
format = 'L'
|
|
|
|
elif mode == 'color':
|
|
|
|
format = 'RGB'
|
|
|
|
else:
|
|
|
|
raise ValueError('got unknown "mode" from self.get_parameters()')
|
|
|
|
im=Image.new(format, (xsize,ysize))
|
|
|
|
self.dev.snap( im.im.id, no_cancel )
|
|
|
|
return im
|
|
|
|
|
|
|
|
def scan(self):
|
|
|
|
self.start()
|
|
|
|
return self.snap()
|
|
|
|
|
|
|
|
def multi_scan(self):
|
|
|
|
return _SaneIterator(self)
|
|
|
|
|
|
|
|
def arr_snap(self, multipleOf=1):
|
|
|
|
"""Snap a picture, returning a numarray object with the results.
|
|
|
|
By default the resulting array has the same number of pixels per
|
|
|
|
line as specified in self.get_parameters()[2][0]
|
|
|
|
However sometimes it is necessary to obtain arrays where
|
|
|
|
the number of pixels per line is e.g. a multiple of 4. This can then
|
|
|
|
be achieved with the option 'multipleOf=4'. So if the scanner
|
|
|
|
scanned 34 pixels per line, you will obtain an array with 32 pixels
|
|
|
|
per line.
|
|
|
|
"""
|
|
|
|
(mode, last_frame, (xsize, ysize), depth, bpl) = self.get_parameters()
|
|
|
|
if not mode in ['gray', 'red', 'green', 'blue']:
|
|
|
|
raise RuntimeError('arr_snap() only works with monochrome images')
|
|
|
|
if multipleOf < 1:
|
|
|
|
raise ValueError('option "multipleOf" must be a positive number')
|
|
|
|
elif multipleOf > 1:
|
|
|
|
pixels_per_line = xsize - divmod(xsize, 4)[1]
|
|
|
|
else:
|
|
|
|
pixels_per_line = xsize
|
|
|
|
return self.dev.arr_snap(pixels_per_line)
|
|
|
|
|
|
|
|
def arr_scan(self, multipleOf=1):
|
|
|
|
self.start()
|
|
|
|
return self.arr_snap(multipleOf=multipleOf)
|
|
|
|
|
|
|
|
def fileno(self):
|
|
|
|
"Return the file descriptor for the scanning device"
|
|
|
|
return self.dev.fileno()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.dev.close()
|
|
|
|
|
|
|
|
|
|
|
|
def open(devname):
|
|
|
|
"Open a device for scanning"
|
|
|
|
new=SaneDev(devname)
|
|
|
|
return new
|