Pillow/Sane/sane.py

289 lines
9.9 KiB
Python
Raw Normal View History

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)
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],
repr(self.constraint), active, settable)
2010-07-31 06:52:47 +04:00
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 as v:
2010-07-31 06:52:47 +04:00
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 key not in optdict:
2010-07-31 06:52:47 +04:00
self.__dict__[key]=value ; return
opt=optdict[key]
if opt.type==TYPE_GROUP:
raise AttributeError("Groups can't be set: "+key)
2010-07-31 06:52:47 +04:00
if not _sane.OPTION_IS_ACTIVE(opt.cap):
raise AttributeError('Inactive option: '+key)
2010-07-31 06:52:47 +04:00
if not _sane.OPTION_IS_SETTABLE(opt.cap):
raise AttributeError("Option can't be set by software: "+key)
2010-07-31 06:52:47 +04:00
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 list(self.opt.keys())
2010-07-31 06:52:47 +04:00
if key=='area':
return (self.tl_x, self.tl_y),(self.br_x, self.br_y)
if key not in optdict:
raise AttributeError('No such attribute: '+key)
2010-07-31 06:52:47 +04:00
opt=optdict[key]
if opt.type==TYPE_BUTTON:
raise AttributeError("Buttons don't have values: "+key)
2010-07-31 06:52:47 +04:00
if opt.type==TYPE_GROUP:
raise AttributeError("Groups don't have values: "+key)
2010-07-31 06:52:47 +04:00
if not _sane.OPTION_IS_ACTIVE(opt.cap):
raise AttributeError('Inactive option: '+key)
2010-07-31 06:52:47 +04:00
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