Patches to sane.py and _sanemodule.c (was: Re: [Image-SIG] Sane: Problems in setting color mode)

abel deuring a.deuring@satzbau-gmbh.de
Sun, 22 Sep 2002 02:15:42 +0200


This is a multi-part message in MIME format.
--------------030403010402060201000401
Content-Type: text/plain; charset=us-ascii; format=flowed
Content-Transfer-Encoding: 7bit

Hi all,

abel deuring wrote:


> But I'm afraid that you won't get very far anyway with the Sane module 
> from PIL 1.1.3. This is the source code of the read loop in SaneDev_Snap 
> from this version:
> 
> SaneDev_snap(self, args)
[...]
> Three other bugs in this code:
> 
> 1. 'SANE_Parameters p' is not initialized (a call to sane_get_parameters 
> is missing), hence 'switch (p.format)' uses an arbtrary value;
> 2. the scan finishes, sane_cancel must be called. (try for example the - 
> relatively new - test backend on this code. Without a call to 
> sane_cancel after sane_start / sane_read; calls of sane_control_option 
> will return errors. The requierement to call sane_cancel is also 
> mentioned in the Sane API documentation);
> 3. If the image aqusistion requires more than one call of sane_read,
> SaneDev_snap will return an error:

Sorry, here I wrote nonsense :( I didn't propoerly read the 'if' 
expression which detects errors.

Anyway, attached are patches to sane.py and _sanemodule.c which fix some 
  problems of the current Sane interface:

- RGB scans (1 pass and 3 pass) are now working again
- a backend may now return padding bytes at the end of a scan line
- somewhat better Sane error handling
- sane.py now reloads the option paramters, if a call to
   sane_control_option returns SANE_INFO_RELOAD_OPTIONS.

These issues are still unresolved;

- support for 1 bit and 16 bit scan data
- handscanner support. I am not sure, if it is worth the effort to
   support handscanners. But sane.snap and/or sane.start should at least
   return an error, if "hand scanner behaviour" (i.e.,
   sane_get_parameters sets the number of scan lines to -1) can be
   detected.
- array options (e.g. gamma tables) are not supported.

Abel

--------------030403010402060201000401
Content-Type: text/plain;
 name="_sanemodule.c.diff"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="_sanemodule.c.diff"

--- _sanemodule.c.orig	Sun Sep 22 01:48:18 2002
+++ _sanemodule.c	Sun Sep 22 01:48:33 2002
@@ -79,7 +79,7 @@
 {
   if (self->h) sane_close(self->h);
   self->h=NULL;
-  PyMem_DEL(self);
+  PyObject_DEL(self);
 }
 
 static PyObject *
@@ -103,7 +103,7 @@
     case(SANE_FRAME_GREEN): format="G"; break;
     case(SANE_FRAME_BLUE): format="B"; break;
     }
-  
+
   return Py_BuildValue("si(ii)ii", format, p.last_frame, p.pixels_per_line, p.lines, 
 		       p.depth, p.bytes_per_line);
 }
@@ -295,6 +295,7 @@
 	  return NULL;
 	}
       memcpy(v, PyString_AsString(value), PyString_Size(value));
+      ((char*)v)[PyString_Size(value)] = 0;
       break;
     case(SANE_TYPE_BUTTON): 
     case(SANE_TYPE_GROUP):
@@ -329,18 +330,23 @@
   return Py_BuildValue("i", i);
  }
 
+#define READSIZE 32768
+#define RED_FRAME 1
+#define GREEN_FRAME 2
+#define BLUE_FRAME 4
+#define ALL_FRAMES 7
+
 static PyObject *
 SaneDev_snap(self, args)
 	SaneDevObject *self;
 	PyObject *args;
 {
   SANE_Status st; 
-  SANE_Byte buffer[8192];  /* XXX how big should the buffer be? */
+  SANE_Byte buffer[READSIZE];  /* XXX how big should the buffer be? */
   SANE_Int len;
   Imaging im;
   SANE_Parameters p;
-  char *format="unknown format";
-  int px, py;
+  int px, py, remain, cplen, bufpos, padbytes, frames = 0;
   long L;
   
   if (!PyArg_ParseTuple(args, "i", &L))
@@ -348,30 +354,242 @@
   im=(Imaging)L;
 
   st=SANE_STATUS_GOOD; px=py=0;
-  while (st!=SANE_STATUS_EOF)
+  /* xxx not yet implemented
+     - lineart scans
+     - scans with more than 8 bit
+       ?? simply return 8 bit and discard the remaining data??
+     - handscanner support (i.e., unknown image length during start)
+     - generally: move the functionality from method snap in sane.py
+       down here -- I don't like this cross-dependency.
+       we need to call sane_get_parameters here, and we can create
+       the result Image object here.
+  */
+  sane_get_parameters(self->h, &p);
+  if (p.format == SANE_FRAME_GRAY)
     {
-      st=sane_read(self->h, buffer, 8192, &len);
-      if (st && (st!=SANE_STATUS_EOF)) return PySane_Error(st);
-      if (st==SANE_STATUS_GOOD)
-	{
-	  if (p.format==SANE_FRAME_RGB) 
-	    {
-	    }
-	  else
-	    {
-	      /* Handle some sort of 8-bit code */
-	      /* XXX Optimize */
-	      int i;
-	      for (i=0; i<len && py <im->ysize; i++)
-		{
-		  im->image8[py][px]=buffer[i];
-		  if (++px >= (int) im->xsize)
-		    px = 0, py++;
-		}
-	    }
-	}
+      remain = p.bytes_per_line * im->ysize;
+      padbytes = p.bytes_per_line - im->xsize;
+      bufpos = 0;
+      while (st!=SANE_STATUS_EOF && remain > 0 && py < im->ysize)
+        {
+          st=sane_read(self->h, buffer, 
+                       remain<READSIZE ? remain : READSIZE, &len);
+          if (st && (st!=SANE_STATUS_EOF))
+            {
+              sane_cancel(self->h);
+              return PySane_Error(st);
+            }
+          remain -= len;
+          /* skip possible pad bytes at the start of the buffer */
+          len -= bufpos;
+          while (len > 0)
+            {
+              cplen = len;
+              if (px + cplen >= im->xsize)
+                  cplen = im->xsize - px;
+              memcpy(&im->image8[py][px], &buffer[bufpos], cplen);
+              len -= cplen;
+              bufpos += cplen;
+              px += cplen;
+              if (px >= im->xsize)
+                {
+                  px = 0;
+                  py++;
+                  bufpos += padbytes;
+                  len -= padbytes;
+                }
+            }
+          bufpos = -len;
+    	}
     }
-
+  else if (p.format == SANE_FRAME_RGB)
+    {
+      int lastlen;
+      remain = p.bytes_per_line * im->ysize;
+      padbytes = p.bytes_per_line - 3 * im->xsize;
+      bufpos = 0;
+      len = 0;
+      lastlen = 0;
+      /* probably not very efficient. But we have to deal with these
+         possible conditions:
+         - we may have padding bytes at the end of a scan line
+         - the number of bytes read with sane_read may be smaller
+           than the number of pad bytes
+         - the buffer may become empty after setting any of the 
+           red/green/blue pixel values
+         
+      */
+      while (st != SANE_STATUS_EOF && py < im->ysize)
+        {
+          while (len <= 0)
+            {
+              bufpos -= lastlen;
+              if (remain == 0)
+                {
+                  PyErr_SetString(ErrorObject, "internal _sane error: premature end of scan");
+                  sane_cancel(self->h);
+                  return NULL;
+                }
+              st = sane_read(self->h, buffer,
+                    remain<(READSIZE) ? remain : (READSIZE), &len);
+              if (st && (st!=SANE_STATUS_EOF))
+                {
+                  sane_cancel(self->h);
+                  return PySane_Error(st);
+                }
+              lastlen = len;
+              remain -= len;
+              if (bufpos >= len)
+                /* skip pad bytes */
+                len = 0;
+              else
+                len -= bufpos;
+            }
+          ((UINT8**)(im->image32))[py][px++] = buffer[bufpos++]; len--;
+          if (!len)
+            {
+              bufpos -= lastlen;
+              st = sane_read(self->h, buffer,
+                    remain<(READSIZE) ? remain : (READSIZE), &len);
+              if (st && (st!=SANE_STATUS_EOF))
+                {
+                  sane_cancel(self->h);
+                  return PySane_Error(st);
+                }
+              remain -= len;
+              lastlen = len;
+            }
+          ((UINT8**)(im->image32))[py][px++] = buffer[bufpos++]; len--;
+          if (!len)
+            {
+              bufpos -= lastlen;
+              st = sane_read(self->h, buffer,
+                    remain<(READSIZE) ? remain : (READSIZE), &len);
+              if (st && (st!=SANE_STATUS_EOF))
+                {
+                  sane_cancel(self->h);
+                  return PySane_Error(st);
+                }
+              remain -= len;
+              lastlen = len;
+            }
+          ((UINT8**)(im->image32))[py][px++] = buffer[bufpos++]; len--;
+          ((UINT8**)(im->image32))[py][px++] = 0;
+
+          if (px >= im->xsize * 4)
+            {
+              px = 0;
+              py++;
+              bufpos += padbytes;
+              len -= padbytes;
+            }
+        }
+    }
+  else /* should be SANE_FRAME_RED, GREEN or BLUE */
+    {
+      int lastlen, pxa, offset;
+      char errmsg[80];
+      
+      while (frames != ALL_FRAMES)
+        {
+          remain = p.bytes_per_line * im->ysize;
+          padbytes = p.bytes_per_line - im->xsize;
+          bufpos = 0;
+          len = 0;
+          lastlen = 0;
+          py = 0;
+          switch (p.format)
+            { 
+              case SANE_FRAME_RED:
+                offset = 0;
+                frames |= RED_FRAME;
+                break;
+              case SANE_FRAME_GREEN:
+                offset = 1;
+                frames |= GREEN_FRAME;
+                break;
+              case SANE_FRAME_BLUE:
+                offset = 2;
+                frames |= BLUE_FRAME;
+                break;
+              default:
+                sane_cancel(self->h);
+                snprintf(errmsg, 80, "unknown/invalid frame format: %i", p.format);
+                PyErr_SetString(ErrorObject, errmsg);
+                return NULL;
+            }
+          px = offset;
+          pxa = 3;
+          st = SANE_STATUS_GOOD;
+          while (st != SANE_STATUS_EOF && py < im->ysize)
+            {
+              while (len <= 0)
+                {
+                  bufpos -= lastlen;
+                  if (remain == 0)
+                    {
+                      PyErr_SetString(ErrorObject, "internal _sane error: premature end of scan");
+                      sane_cancel(self->h);
+                      return NULL;
+                    }
+                  st = sane_read(self->h, buffer,
+                        remain<(READSIZE) ? remain : (READSIZE), &len);
+                  if (st && (st!=SANE_STATUS_EOF))
+                    {
+                      sane_cancel(self->h);
+                      return PySane_Error(st);
+                    }
+                  lastlen = len;
+                  remain -= len;
+                  if (bufpos >= len)
+                    /* skip pad bytes */
+                    len = 0;
+                  else
+                    len -= bufpos;
+                }
+              ((UINT8**)(im->image32))[py][px] = buffer[bufpos++]; len--;
+              ((UINT8**)(im->image32))[py][pxa] = 0;
+              px += 4;
+              pxa += 4;
+
+              if (px >= im->xsize * 4)
+                {
+                  px = offset;
+                  pxa = 3;
+                  py++;
+                  bufpos += padbytes;
+                  len -= padbytes;
+                }
+            }
+          if (!p.last_frame)
+            {
+              /* the sane_read calls in the above loop should return
+                 SANE_STATUS_GOOD, but backend may need another sane_read
+                 call which returns SANE_STATUS_EOF in order to start
+                 a new frame.
+              */
+              do {
+                   st = sane_read(self->h, buffer, READSIZE, &len);
+                 }
+              while (st == SANE_STATUS_GOOD);
+              if (st != SANE_STATUS_EOF)
+                {
+                   sane_cancel(self->h);
+                   return PySane_Error(st);
+                }
+              
+              st = sane_start(self->h);
+              if (st) return PySane_Error(st);
+              st = sane_get_parameters(self->h, &p);
+              if (st)
+                {
+                  sane_cancel(self->h);
+                  return PySane_Error(st);
+                }
+            }
+        }
+    }
+  sane_cancel(self->h);
   Py_INCREF(Py_None);
   return Py_None;
 }
@@ -634,6 +852,7 @@
 	insint(d, "UNIT_MM", SANE_UNIT_MM);
 	insint(d, "UNIT_DPI", SANE_UNIT_DPI);
 	insint(d, "UNIT_PERCENT", SANE_UNIT_PERCENT);
+	insint(d, "UNIT_MICROSECOND", SANE_UNIT_MICROSECOND);
 
 	insint(d, "CAP_SOFT_SELECT", SANE_CAP_SOFT_SELECT);
 	insint(d, "CAP_HARD_SELECT", SANE_CAP_HARD_SELECT);
@@ -642,6 +861,9 @@
 	insint(d, "CAP_AUTOMATIC", SANE_CAP_AUTOMATIC);
 	insint(d, "CAP_INACTIVE", SANE_CAP_INACTIVE);
 	insint(d, "CAP_ADVANCED", SANE_CAP_ADVANCED);
+
+	/* handy for checking array lengths: */
+	insint(d, "SANE_WORD_SIZE", sizeof(SANE_Word));
 
 	/* Check for errors */
 	if (PyErr_Occurred())

--------------030403010402060201000401
Content-Type: text/plain;
 name="sane.py.diff"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="sane.py.diff"

--- sane.py.orig	Sun Sep 22 01:48:02 2002
+++ sane.py	Sun Sep 22 01:53:43 2002
@@ -1,34 +1,47 @@
 
+import string
 import Image
 import _sane
 
 from _sane import *
 
+def _f(x):
+    if x=='-': return '_'
+    else: return x
+    
+def _py_name(s):
+    return string.join(map(_f, s), '')
+
 class Option:
     def __init__(self, args):
-	import string
+        self.set_param(args)
+    
+    def set_param(self, args):
 	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=string.join(map(f, self.name), '')
+	else: self.py_name=string.join(map(_f, self.name), '')
 
 class SaneDev:
     def __init__(self, devname): 
 	d=self.__dict__
 	d['dev']=_sane._open(devname)
 	d['opt']={} 
-
-	optlist=d['dev'].get_options()
+	optlist = self.dev.get_options()
 	for t in optlist:
-	    o=Option(t)
+	    o = Option(t)
 	    if o.type!=TYPE_GROUP:
-		d['opt'][o.py_name]=o
+	        # xxx would be better to check if an option name is present!
+		self.opt[o.py_name]=o
+
+    def reload_opts(self):
+        optlist = self.dev.get_options()
+        for t in optlist:
+            if t[4] != TYPE_GROUP and t[1]:
+                self.opt[_py_name(t[1])].set_param(t)
 
     def __setattr__(self, key, value):
 	dev=self.__dict__['dev']
@@ -44,6 +57,8 @@
 	    raise AttributeError, "Option can't be set by software: "+key	
 
 	self.last_opt = dev.set_option(opt.index, value)
+	if self.last_opt & INFO_RELOAD_OPTIONS:
+	    self.reload_opts()
 	
     def __getattr__(self, key):
 	dev=self.__dict__['dev']
@@ -58,6 +73,8 @@
 	if not _sane.OPTION_IS_ACTIVE(opt.cap):
 	    raise AttributeError, 'Inactive option: '+key
 	self.last_opt, value = dev.get_option(opt.index)
+	if self.last_opt & INFO_RELOAD_OPTIONS:
+	    self.reload_opts()
 	return value
 
     def get_parameters(self): return self.dev.get_parameters()
@@ -67,6 +84,8 @@
     def fileno(self): return self.dev.fileno()
     def snap(self):
 	format, last_frame, (xsize, ysize), depth, bytes_per_line = self.get_parameters()
+	if format in ('R', 'G', 'B'):
+	    format = 'RGB'
 	im=Image.new(format, (xsize,ysize))
 	self.dev.snap( im.im.id )
 	return im

--------------030403010402060201000401--