# 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 not isinstance(self.name, str): self.py_name=str(self.name) else: self.py_name=''.join(map(f, self.name)) 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 = '' 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) 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: 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: 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 isinstance(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()) 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) 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