kopia lustrzana https://gitlab.com/sane-project/backends
1108 wiersze
31 KiB
C
1108 wiersze
31 KiB
C
/* sane - Scanner Access Now Easy.
|
|
Copyright (C) 1999 Juergen G. Schimmer
|
|
Updates and bugfixes (C) 2002 Henning Meier-Geinitz
|
|
|
|
This file is part of the SANE package.
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place - Suite 330, Boston,
|
|
MA 02111-1307, USA.
|
|
|
|
As a special exception, the authors of SANE give permission for
|
|
additional uses of the libraries contained in this release of SANE.
|
|
|
|
The exception is that, if you link a SANE library with other files
|
|
to produce an executable, this does not by itself cause the
|
|
resulting executable to be covered by the GNU General Public
|
|
License. Your use of that executable is in no way restricted on
|
|
account of linking the SANE library code into it.
|
|
|
|
This exception does not, however, invalidate any other reasons why
|
|
the executable file might be covered by the GNU General Public
|
|
License.
|
|
|
|
If you submit changes to SANE to the maintainers to be included in
|
|
a subsequent release, you agree by submitting the changes that
|
|
those changes may be distributed with this exception intact.
|
|
|
|
If you write modifications of your own for SANE, it is your choice
|
|
whether to permit this exception to apply to your modifications.
|
|
If you do not wish that, delete this exception notice.
|
|
|
|
This file implements a SANE backend for v4l-Devices.
|
|
*/
|
|
|
|
#define BUILD 2
|
|
|
|
#include "../include/sane/config.h"
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <setjmp.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
|
|
#include <unistd.h>
|
|
#include <sys/time.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "../include/sane/sane.h"
|
|
#include "../include/sane/sanei.h"
|
|
#include "../include/sane/saneopts.h"
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <asm/types.h> /* XXX glibc */
|
|
#include <linux/videodev.h>
|
|
|
|
#include "v4l.h"
|
|
|
|
#define BACKEND_NAME v4l
|
|
#include "../include/sane/sanei_backend.h"
|
|
|
|
#ifndef PATH_MAX
|
|
# define PATH_MAX 1024
|
|
#endif
|
|
|
|
#include "../include/sane/sanei_config.h"
|
|
#define V4L_CONFIG_FILE "v4l.conf"
|
|
|
|
static const SANE_Device **devlist = NULL;
|
|
static int num_devices;
|
|
static V4L_Device *first_dev;
|
|
static V4L_Scanner *first_handle;
|
|
static char *buffer;
|
|
|
|
static const SANE_String_Const mode_list[] = {
|
|
"Grey", "Color",
|
|
0
|
|
};
|
|
|
|
static const SANE_Range u8_range = {
|
|
/* min, max, quantization */
|
|
0, 255, 0
|
|
};
|
|
|
|
static SANE_Range x_range = { 0, 338, 2 };
|
|
|
|
static SANE_Range odd_x_range = { 1, 339, 2 };
|
|
|
|
static SANE_Range y_range = { 0, 249, 1 };
|
|
|
|
static SANE_Range odd_y_range = { 1, 250, 1 };
|
|
|
|
|
|
static SANE_Parameters parms = {
|
|
SANE_FRAME_RGB,
|
|
1, /* 1 = Last Frame , 0 = More Frames to come */
|
|
0, /* Number of bytes returned per scan line: */
|
|
0, /* Number of pixels per scan line. */
|
|
0, /* Number of lines for the current scan. */
|
|
8, /* Number of bits per sample. */
|
|
};
|
|
|
|
static SANE_Status
|
|
attach (const char *devname, V4L_Device ** devp)
|
|
{
|
|
V4L_Device *dev;
|
|
static int v4lfd;
|
|
static struct video_capability capability;
|
|
|
|
errno = 0;
|
|
|
|
for (dev = first_dev; dev; dev = dev->next)
|
|
if (strcmp (dev->sane.name, devname) == 0)
|
|
{
|
|
if (devp)
|
|
*devp = dev;
|
|
DBG (5, "attach: device %s is already known\n", devname);
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
DBG (3, "attach: trying to open %s\n", devname);
|
|
v4lfd = open (devname, O_RDWR);
|
|
if (v4lfd != -1)
|
|
{
|
|
if (ioctl (v4lfd, VIDIOCGCAP, &capability) == -1)
|
|
{
|
|
DBG (1,
|
|
"attach: ioctl (%d, VIDIOCGCAP,..) failed on `%s': %s\n",
|
|
v4lfd, devname, strerror (errno));
|
|
close (v4lfd);
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
if (!(VID_TYPE_CAPTURE & capability.type))
|
|
{
|
|
DBG (1, "attach: device %s can't capture to memory -- exiting\n",
|
|
devname);
|
|
close (v4lfd);
|
|
return SANE_STATUS_UNSUPPORTED;
|
|
}
|
|
DBG (2, "attach: found videodev `%s' on `%s'\n", capability.name,
|
|
devname);
|
|
close (v4lfd);
|
|
}
|
|
else
|
|
{
|
|
DBG (1, "attach: failed to open device `%s': %s\n", devname,
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
dev = malloc (sizeof (*dev));
|
|
if (!dev)
|
|
return SANE_STATUS_NO_MEM;
|
|
|
|
memset (dev, 0, sizeof (*dev));
|
|
|
|
dev->sane.name = strdup (devname);
|
|
if (!dev->sane.name)
|
|
return SANE_STATUS_NO_MEM;
|
|
dev->sane.vendor = "Noname";
|
|
dev->sane.model = capability.name;
|
|
dev->sane.type = "virtual device";
|
|
|
|
++num_devices;
|
|
dev->next = first_dev;
|
|
first_dev = dev;
|
|
|
|
if (devp)
|
|
*devp = dev;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
static void
|
|
update_parameters (V4L_Scanner * s)
|
|
{
|
|
/* ??? should be per-device */
|
|
x_range.min = 0;
|
|
x_range.max = s->capability.maxwidth - s->capability.minwidth;
|
|
x_range.quant = 1;
|
|
|
|
y_range.min = 0;
|
|
y_range.max = s->capability.maxheight - s->capability.minheight;
|
|
y_range.quant = 1;
|
|
|
|
odd_x_range.min = s->capability.minwidth;
|
|
odd_x_range.max = s->capability.maxwidth;
|
|
if (odd_x_range.max > 767)
|
|
{
|
|
odd_x_range.max = 767;
|
|
x_range.max = 767 - s->capability.minwidth;
|
|
};
|
|
odd_x_range.quant = 1;
|
|
|
|
odd_y_range.min = s->capability.minheight;
|
|
odd_y_range.max = s->capability.maxheight;
|
|
if (odd_y_range.max > 511)
|
|
{
|
|
odd_y_range.max = 511;
|
|
y_range.max = 511 - s->capability.minheight;
|
|
};
|
|
odd_y_range.quant = 1;
|
|
|
|
parms.lines = s->window.height;
|
|
parms.pixels_per_line = s->window.width;
|
|
|
|
switch (s->pict.palette)
|
|
{
|
|
case VIDEO_PALETTE_GREY: /* Linear greyscale */
|
|
{
|
|
parms.format = SANE_FRAME_GRAY;
|
|
parms.depth = 8;
|
|
parms.bytes_per_line = s->window.width;
|
|
break;
|
|
}
|
|
case VIDEO_PALETTE_RGB24: /* 24bit RGB */
|
|
{
|
|
parms.format = SANE_FRAME_RGB;
|
|
parms.depth = 8;
|
|
parms.bytes_per_line = s->window.width * 3;
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
parms.format = SANE_FRAME_GRAY;
|
|
parms.bytes_per_line = s->window.width;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static SANE_Status
|
|
init_options (V4L_Scanner * s)
|
|
{
|
|
int i;
|
|
|
|
memset (s->opt, 0, sizeof (s->opt));
|
|
memset (s->val, 0, sizeof (s->val));
|
|
|
|
for (i = 0; i < NUM_OPTIONS; ++i)
|
|
{
|
|
s->opt[i].size = sizeof (SANE_Word);
|
|
s->opt[i].cap = (SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT
|
|
| SANE_CAP_ALWAYS_SETTABLE);
|
|
}
|
|
|
|
/* Number of options */
|
|
s->opt[OPT_NUM_OPTS].title = SANE_TITLE_NUM_OPTIONS;
|
|
s->opt[OPT_NUM_OPTS].desc = SANE_DESC_NUM_OPTIONS;
|
|
s->opt[OPT_NUM_OPTS].type = SANE_TYPE_INT;
|
|
s->opt[OPT_NUM_OPTS].cap = SANE_CAP_SOFT_DETECT;
|
|
s->val[OPT_NUM_OPTS].w = NUM_OPTIONS;
|
|
|
|
/* "Mode" group: */
|
|
s->opt[OPT_MODE_GROUP].title = "Scan Mode";
|
|
s->opt[OPT_MODE_GROUP].desc = "";
|
|
s->opt[OPT_MODE_GROUP].type = SANE_TYPE_GROUP;
|
|
s->opt[OPT_MODE_GROUP].cap = 0;
|
|
s->opt[OPT_MODE_GROUP].constraint_type = SANE_CONSTRAINT_NONE;
|
|
|
|
/* mode */
|
|
s->opt[OPT_MODE].name = SANE_NAME_SCAN_MODE;
|
|
s->opt[OPT_MODE].title = SANE_TITLE_SCAN_MODE;
|
|
s->opt[OPT_MODE].desc = SANE_DESC_SCAN_MODE;
|
|
s->opt[OPT_MODE].type = SANE_TYPE_STRING;
|
|
s->opt[OPT_MODE].unit = SANE_UNIT_NONE;
|
|
s->opt[OPT_MODE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
|
|
s->opt[OPT_MODE].constraint.string_list = mode_list;
|
|
s->val[OPT_MODE].s = strdup (mode_list[0]);
|
|
if (!s->val[OPT_MODE].s)
|
|
return SANE_STATUS_NO_MEM;
|
|
|
|
/* channel */
|
|
s->opt[OPT_CHANNEL].name = "channel";
|
|
s->opt[OPT_CHANNEL].title = "Channel";
|
|
s->opt[OPT_CHANNEL].desc =
|
|
"Selects the channel of the v4l device (e.g. television " "or video-in.";
|
|
s->opt[OPT_CHANNEL].type = SANE_TYPE_STRING;
|
|
s->opt[OPT_CHANNEL].unit = SANE_UNIT_NONE;
|
|
s->opt[OPT_CHANNEL].constraint_type = SANE_CONSTRAINT_STRING_LIST;
|
|
s->opt[OPT_CHANNEL].constraint.string_list = s->channel;
|
|
s->val[OPT_CHANNEL].s = strdup (s->channel[0]);
|
|
if (!s->val[OPT_CHANNEL].s)
|
|
return SANE_STATUS_NO_MEM;
|
|
if (s->channel[0] == 0 || s->channel[1] == 0)
|
|
s->opt[OPT_CHANNEL].cap |= SANE_CAP_INACTIVE;
|
|
|
|
/* "Geometry" group: */
|
|
s->opt[OPT_GEOMETRY_GROUP].title = "Geometry";
|
|
s->opt[OPT_GEOMETRY_GROUP].desc = "";
|
|
s->opt[OPT_GEOMETRY_GROUP].type = SANE_TYPE_GROUP;
|
|
s->opt[OPT_GEOMETRY_GROUP].cap = SANE_CAP_ADVANCED;
|
|
s->opt[OPT_GEOMETRY_GROUP].constraint_type = SANE_CONSTRAINT_NONE;
|
|
|
|
/* top-left x *//* ??? first check if window is settable at all */
|
|
s->opt[OPT_TL_X].name = SANE_NAME_SCAN_TL_X;
|
|
s->opt[OPT_TL_X].title = SANE_TITLE_SCAN_TL_X;
|
|
s->opt[OPT_TL_X].desc = SANE_DESC_SCAN_TL_X;
|
|
s->opt[OPT_TL_X].type = SANE_TYPE_INT;
|
|
s->opt[OPT_TL_X].unit = SANE_UNIT_PIXEL;
|
|
s->opt[OPT_TL_X].cap |= SANE_CAP_INACTIVE;
|
|
s->opt[OPT_TL_X].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_TL_X].constraint.range = &x_range;
|
|
s->val[OPT_TL_X].w = 0;
|
|
|
|
/* top-left y */
|
|
s->opt[OPT_TL_Y].name = SANE_NAME_SCAN_TL_Y;
|
|
s->opt[OPT_TL_Y].title = SANE_TITLE_SCAN_TL_Y;
|
|
s->opt[OPT_TL_Y].desc = SANE_DESC_SCAN_TL_Y;
|
|
s->opt[OPT_TL_Y].type = SANE_TYPE_INT;
|
|
s->opt[OPT_TL_Y].unit = SANE_UNIT_PIXEL;
|
|
s->opt[OPT_TL_Y].cap |= SANE_CAP_INACTIVE;
|
|
s->opt[OPT_TL_Y].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_TL_Y].constraint.range = &y_range;
|
|
s->val[OPT_TL_Y].w = 0;
|
|
|
|
/* bottom-right x */
|
|
s->opt[OPT_BR_X].name = SANE_NAME_SCAN_BR_X;
|
|
s->opt[OPT_BR_X].title = SANE_TITLE_SCAN_BR_X;
|
|
s->opt[OPT_BR_X].desc = SANE_DESC_SCAN_BR_X;
|
|
s->opt[OPT_BR_X].type = SANE_TYPE_INT;
|
|
s->opt[OPT_BR_X].unit = SANE_UNIT_PIXEL;
|
|
s->opt[OPT_BR_X].cap |= SANE_CAP_INACTIVE;
|
|
s->opt[OPT_BR_X].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_BR_X].constraint.range = &odd_x_range;
|
|
s->val[OPT_BR_X].w = s->capability.maxwidth;
|
|
if (s->val[OPT_BR_X].w > 767)
|
|
s->val[OPT_BR_X].w = 767;
|
|
|
|
/* bottom-right y */
|
|
s->opt[OPT_BR_Y].name = SANE_NAME_SCAN_BR_Y;
|
|
s->opt[OPT_BR_Y].title = SANE_TITLE_SCAN_BR_Y;
|
|
s->opt[OPT_BR_Y].desc = SANE_DESC_SCAN_BR_Y;
|
|
s->opt[OPT_BR_Y].type = SANE_TYPE_INT;
|
|
s->opt[OPT_BR_Y].unit = SANE_UNIT_PIXEL;
|
|
s->opt[OPT_BR_Y].cap |= SANE_CAP_INACTIVE;
|
|
s->opt[OPT_BR_Y].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_BR_Y].constraint.range = &odd_y_range;
|
|
s->val[OPT_BR_Y].w = s->capability.maxheight;
|
|
if (s->val[OPT_BR_Y].w > 511)
|
|
s->val[OPT_BR_Y].w = 511;
|
|
|
|
/* "Enhancement" group: */
|
|
s->opt[OPT_ENHANCEMENT_GROUP].title = "Enhancement";
|
|
s->opt[OPT_ENHANCEMENT_GROUP].desc = "";
|
|
s->opt[OPT_ENHANCEMENT_GROUP].type = SANE_TYPE_GROUP;
|
|
s->opt[OPT_ENHANCEMENT_GROUP].cap = 0;
|
|
s->opt[OPT_ENHANCEMENT_GROUP].constraint_type = SANE_CONSTRAINT_NONE;
|
|
|
|
/* brightness */
|
|
s->opt[OPT_BRIGHTNESS].name = SANE_NAME_BRIGHTNESS;
|
|
s->opt[OPT_BRIGHTNESS].title = SANE_TITLE_BRIGHTNESS;
|
|
s->opt[OPT_BRIGHTNESS].desc = SANE_DESC_BRIGHTNESS;
|
|
s->opt[OPT_BRIGHTNESS].type = SANE_TYPE_INT;
|
|
s->opt[OPT_BRIGHTNESS].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_BRIGHTNESS].constraint.range = &u8_range;
|
|
s->val[OPT_BRIGHTNESS].w = s->pict.brightness / 256;
|
|
|
|
/* hue */
|
|
s->opt[OPT_HUE].name = SANE_NAME_HUE;
|
|
s->opt[OPT_HUE].title = SANE_TITLE_HUE;
|
|
s->opt[OPT_HUE].desc = SANE_DESC_HUE;
|
|
s->opt[OPT_HUE].type = SANE_TYPE_INT;
|
|
s->opt[OPT_HUE].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_HUE].constraint.range = &u8_range;
|
|
s->val[OPT_HUE].w = s->pict.hue / 256;
|
|
|
|
/* colour */
|
|
s->opt[OPT_COLOR].name = "color";
|
|
s->opt[OPT_COLOR].title = "Picture color";
|
|
s->opt[OPT_COLOR].desc = "Sets the picture's color.";
|
|
s->opt[OPT_COLOR].type = SANE_TYPE_INT;
|
|
s->opt[OPT_COLOR].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_COLOR].constraint.range = &u8_range;
|
|
s->val[OPT_COLOR].w = s->pict.colour / 256;
|
|
|
|
/* contrast */
|
|
s->opt[OPT_CONTRAST].name = SANE_NAME_CONTRAST;
|
|
s->opt[OPT_CONTRAST].title = SANE_TITLE_CONTRAST;
|
|
s->opt[OPT_CONTRAST].desc = SANE_DESC_CONTRAST;
|
|
s->opt[OPT_CONTRAST].type = SANE_TYPE_INT;
|
|
s->opt[OPT_CONTRAST].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_CONTRAST].constraint.range = &u8_range;
|
|
s->val[OPT_CONTRAST].w = s->pict.contrast / 256;
|
|
|
|
/* whiteness */
|
|
s->opt[OPT_WHITE_LEVEL].name = SANE_NAME_WHITE_LEVEL;
|
|
s->opt[OPT_WHITE_LEVEL].title = SANE_TITLE_WHITE_LEVEL;
|
|
s->opt[OPT_WHITE_LEVEL].desc = SANE_DESC_WHITE_LEVEL;
|
|
s->opt[OPT_WHITE_LEVEL].type = SANE_TYPE_INT;
|
|
s->opt[OPT_WHITE_LEVEL].constraint_type = SANE_CONSTRAINT_RANGE;
|
|
s->opt[OPT_WHITE_LEVEL].constraint.range = &u8_range;
|
|
s->val[OPT_WHITE_LEVEL].w = s->pict.whiteness / 256;
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_init (SANE_Int * version_code, SANE_Auth_Callback authorize)
|
|
{
|
|
char dev_name[PATH_MAX], *str;
|
|
size_t len;
|
|
FILE *fp;
|
|
|
|
authorize = authorize; /* stop gcc from complaining */
|
|
DBG_INIT ();
|
|
|
|
DBG (2, "SANE v4l backend version %d.%d build %d from %s\n", V_MAJOR,
|
|
V_MINOR, BUILD, PACKAGE_STRING);
|
|
|
|
if (version_code)
|
|
*version_code = SANE_VERSION_CODE (V_MAJOR, V_MINOR, BUILD);
|
|
|
|
fp = sanei_config_open (V4L_CONFIG_FILE);
|
|
if (!fp)
|
|
{
|
|
DBG (2,
|
|
"sane_init: file `%s' not accessible (%s), trying /dev/video0\n",
|
|
V4L_CONFIG_FILE, strerror (errno));
|
|
|
|
return attach ("/dev/video0", 0);
|
|
}
|
|
|
|
while (sanei_config_read (dev_name, sizeof (dev_name), fp))
|
|
{
|
|
if (dev_name[0] == '#') /* ignore line comments */
|
|
continue;
|
|
len = strlen (dev_name);
|
|
|
|
if (!len)
|
|
continue; /* ignore empty lines */
|
|
|
|
/* Remove trailing space and trailing comments */
|
|
for (str = dev_name; *str && !isspace (*str) && *str != '#'; ++str);
|
|
attach (dev_name, 0);
|
|
}
|
|
fclose (fp);
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
void
|
|
sane_exit (void)
|
|
{
|
|
V4L_Device *dev, *next;
|
|
|
|
for (dev = first_dev; dev; dev = next)
|
|
{
|
|
next = dev->next;
|
|
free ((void *) dev->sane.name);
|
|
free (dev);
|
|
}
|
|
|
|
if (NULL != devlist)
|
|
{
|
|
free (devlist);
|
|
devlist = NULL;
|
|
}
|
|
DBG (5, "sane_exit: all devices freed\n");
|
|
}
|
|
|
|
SANE_Status
|
|
sane_get_devices (const SANE_Device *** device_list, SANE_Bool local_only)
|
|
{
|
|
V4L_Device *dev;
|
|
int i;
|
|
|
|
DBG (5, "sane_get_devices\n");
|
|
local_only = SANE_TRUE; /* Avoid compile warning */
|
|
|
|
if (devlist)
|
|
free (devlist);
|
|
|
|
devlist = malloc ((num_devices + 1) * sizeof (devlist[0]));
|
|
if (!devlist)
|
|
return SANE_STATUS_NO_MEM;
|
|
|
|
i = 0;
|
|
for (dev = first_dev; i < num_devices; dev = dev->next)
|
|
devlist[i++] = &dev->sane;
|
|
devlist[i++] = 0;
|
|
|
|
*device_list = devlist;
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_open (SANE_String_Const devname, SANE_Handle * handle)
|
|
{
|
|
V4L_Device *dev;
|
|
V4L_Scanner *s;
|
|
static int v4lfd;
|
|
int i;
|
|
struct video_channel channel;
|
|
SANE_Status status;
|
|
int max_channels = MAX_CHANNELS;
|
|
|
|
if (!devname)
|
|
{
|
|
DBG (1, "sane_open: devname == 0\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
for (dev = first_dev; dev; dev = dev->next)
|
|
if (strcmp (dev->sane.name, devname) == 0)
|
|
{
|
|
DBG (5, "sane_open: device %s found in devlist\n", devname);
|
|
break;
|
|
}
|
|
if (!devname[0])
|
|
dev = first_dev;
|
|
if (!dev)
|
|
{
|
|
DBG (1, "sane_open: device %s doesn't seem to be a v4l "
|
|
"device\n", devname);
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
v4lfd = open (devname, O_RDWR);
|
|
if (v4lfd == -1)
|
|
{
|
|
DBG (1, "sane_open: can't open %s (%s)\n", devname, strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
s = malloc (sizeof (*s));
|
|
if (!s)
|
|
return SANE_STATUS_NO_MEM;
|
|
memset (s, 0, sizeof (*s));
|
|
s->user_corner = 0; /* ??? */
|
|
s->devicename = devname;
|
|
s->fd = v4lfd;
|
|
|
|
if (ioctl (s->fd, VIDIOCGCAP, &s->capability) == -1)
|
|
{
|
|
DBG (1, "sane_open: ioctl (%d, VIDIOCGCAP,..) failed on `%s': %s\n",
|
|
s->fd, devname, strerror (errno));
|
|
close (s->fd);
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
DBG (5, "sane_open: %d channels, %d audio devices\n",
|
|
s->capability.channels, s->capability.audios);
|
|
DBG (5, "sane_open: minwidth=%d, minheight=%d, maxwidth=%d, "
|
|
"maxheight=%d\n", s->capability.minwidth, s->capability.minheight,
|
|
s->capability.maxwidth, s->capability.maxheight);
|
|
if (VID_TYPE_CAPTURE & s->capability.type)
|
|
DBG (5, "sane_open: V4L device can capture to memory\n");
|
|
if (VID_TYPE_TUNER & s->capability.type)
|
|
DBG (5, "sane_open: V4L device has a tuner of some form\n");
|
|
if (VID_TYPE_TELETEXT & s->capability.type)
|
|
DBG (5, "sane_open: V4L device supports teletext\n");
|
|
if (VID_TYPE_OVERLAY & s->capability.type)
|
|
DBG (5, "sane_open: V4L device can overlay its image onto the frame "
|
|
"buffer\n");
|
|
if (VID_TYPE_CHROMAKEY & s->capability.type)
|
|
DBG (5, "sane_open: V4L device uses chromakey on overlay\n");
|
|
if (VID_TYPE_CLIPPING & s->capability.type)
|
|
DBG (5, "sane_open: V4L device supports overlay clipping\n");
|
|
if (VID_TYPE_FRAMERAM & s->capability.type)
|
|
DBG (5, "sane_open: V4L device overwrites frame buffer memory\n");
|
|
if (VID_TYPE_SCALES & s->capability.type)
|
|
DBG (5, "sane_open: V4L device supports hardware scaling\n");
|
|
if (VID_TYPE_MONOCHROME & s->capability.type)
|
|
DBG (5, "sane_open: V4L device is grey scale only\n");
|
|
if (VID_TYPE_SUBCAPTURE & s->capability.type)
|
|
DBG (5, "sane_open: V4L device can capture parts of the image\n");
|
|
|
|
if (s->capability.channels < max_channels)
|
|
max_channels = s->capability.channels;
|
|
for (i = 0; i < max_channels; i++)
|
|
{
|
|
channel.channel = i;
|
|
if (-1 == ioctl (v4lfd, VIDIOCGCHAN, &channel))
|
|
{
|
|
DBG (1, "sane_open: can't ioctl VIDIOCGCHAN %s: %s\n", devname,
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
DBG (5, "sane_open: channel %d (%s), tuners=%d, flags=0x%x, "
|
|
"type=%d, norm=%d\n", channel.channel, channel.name,
|
|
channel.tuners, channel.flags, channel.type, channel.norm);
|
|
if (VIDEO_VC_TUNER & channel.flags)
|
|
DBG (5, "sane_open: channel has tuner(s)\n");
|
|
if (VIDEO_VC_AUDIO & channel.flags)
|
|
DBG (5, "sane_open: channel has audio\n");
|
|
if (VIDEO_TYPE_TV == channel.type)
|
|
DBG (5, "sane_open: input is TV input\n");
|
|
if (VIDEO_TYPE_CAMERA == channel.type)
|
|
DBG (5, "sane_open: input is camera input\n");
|
|
s->channel[i] = strdup (channel.name);
|
|
if (!s->channel[i])
|
|
return SANE_STATUS_NO_MEM;
|
|
}
|
|
s->channel[i] = 0;
|
|
if (-1 == ioctl (v4lfd, VIDIOCGPICT, &s->pict))
|
|
{
|
|
DBG (1, "sane_open: can't ioctl VIDIOCGPICT %s: %s\n", devname,
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
DBG (5, "sane_open: brightness=%d, hue=%d, colour=%d, contrast=%d\n",
|
|
s->pict.brightness, s->pict.hue, s->pict.colour, s->pict.contrast);
|
|
DBG (5, "sane_open: whiteness=%d, depth=%d, palette=%d\n",
|
|
s->pict.whiteness, s->pict.depth, s->pict.palette);
|
|
|
|
/* ??? */
|
|
s->pict.palette = VIDEO_PALETTE_GREY;
|
|
if (-1 == ioctl (s->fd, VIDIOCSPICT, &s->pict))
|
|
{
|
|
DBG (1, "sane_open: ioctl VIDIOCSPICT failed (%s)\n", strerror (errno));
|
|
}
|
|
|
|
if (-1 == ioctl (s->fd, VIDIOCGWIN, &s->window))
|
|
{
|
|
DBG (1, "sane_open: can't ioctl VIDIOCGWIN %s: %s\n", devname,
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
DBG (5, "sane_open: x=%d, y=%d, width=%d, height=%d\n",
|
|
s->window.x, s->window.y, s->window.width, s->window.height);
|
|
|
|
/* already done in sane_start
|
|
if (-1 == ioctl (v4lfd, VIDIOCGMBUF, &mbuf))
|
|
DBG (1, "sane_open: can't ioctl VIDIOCGMBUF (no Fbuffer?)\n");
|
|
*/
|
|
|
|
status = init_options (s);
|
|
if (status != SANE_STATUS_GOOD)
|
|
return status;
|
|
update_parameters (s);
|
|
|
|
/* insert newly opened handle into list of open handles: */
|
|
s->next = first_handle;
|
|
first_handle = s;
|
|
|
|
*handle = s;
|
|
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
void
|
|
sane_close (SANE_Handle handle)
|
|
{
|
|
V4L_Scanner *prev, *s;
|
|
|
|
DBG (2, "sane_close: trying to close handle %p\n", (void *) handle);
|
|
/* remove handle from list of open handles: */
|
|
prev = 0;
|
|
for (s = first_handle; s; s = s->next)
|
|
{
|
|
if (s == handle)
|
|
break;
|
|
prev = s;
|
|
}
|
|
if (!s)
|
|
{
|
|
DBG (1, "sane_close: bad handle %p\n", handle);
|
|
return; /* oops, not a handle we know about */
|
|
}
|
|
if (prev)
|
|
prev->next = s->next;
|
|
else
|
|
first_handle = s->next;
|
|
|
|
if (s->scanning)
|
|
sane_cancel (handle);
|
|
close (s->fd);
|
|
free (s);
|
|
}
|
|
|
|
const SANE_Option_Descriptor *
|
|
sane_get_option_descriptor (SANE_Handle handle, SANE_Int option)
|
|
{
|
|
V4L_Scanner *s = handle;
|
|
|
|
if ((unsigned) option >= NUM_OPTIONS || option < 0)
|
|
return 0;
|
|
DBG (4, "sane_get_option_descriptor: option %d (%s)\n", option,
|
|
s->opt[option].name ? s->opt[option].name : s->opt[option].title);
|
|
return s->opt + option;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_control_option (SANE_Handle handle, SANE_Int option,
|
|
SANE_Action action, void *val, SANE_Int * info)
|
|
{
|
|
V4L_Scanner *s = handle;
|
|
SANE_Status status;
|
|
SANE_Word cap;
|
|
|
|
if (info)
|
|
*info = 0;
|
|
|
|
if (option >= NUM_OPTIONS || option < 0)
|
|
return SANE_STATUS_INVAL;
|
|
|
|
DBG (4, "sane_control_option: %s option %d (%s)\n",
|
|
action == SANE_ACTION_GET_VALUE ? "get" :
|
|
action == SANE_ACTION_SET_VALUE ? "set" :
|
|
action == SANE_ACTION_SET_AUTO ? "auto set" :
|
|
"(unknow action with)", option,
|
|
s->opt[option].name ? s->opt[option].name : s->opt[option].title);
|
|
|
|
cap = s->opt[option].cap;
|
|
|
|
if (!SANE_OPTION_IS_ACTIVE (cap))
|
|
{
|
|
DBG (1, "sane_control option: option is inactive\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
if (action == SANE_ACTION_GET_VALUE)
|
|
{
|
|
switch (option)
|
|
{
|
|
/* word options: */
|
|
case OPT_NUM_OPTS:
|
|
case OPT_TL_X:
|
|
case OPT_TL_Y:
|
|
case OPT_BR_X:
|
|
case OPT_BR_Y:
|
|
case OPT_BRIGHTNESS:
|
|
case OPT_HUE:
|
|
case OPT_COLOR:
|
|
case OPT_CONTRAST:
|
|
case OPT_WHITE_LEVEL:
|
|
*(SANE_Word *) val = s->val[option].w;
|
|
return SANE_STATUS_GOOD;
|
|
case OPT_CHANNEL: /* string list options */
|
|
case OPT_MODE:
|
|
strcpy (val, s->val[option].s);
|
|
return SANE_STATUS_GOOD;
|
|
default:
|
|
DBG (1, "sane_control_option: option %d unknown\n", option);
|
|
}
|
|
}
|
|
else if (action == SANE_ACTION_SET_VALUE)
|
|
{
|
|
if (!SANE_OPTION_IS_SETTABLE (cap))
|
|
{
|
|
DBG (1, "sane_control_option: option is not settable\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
status = sanei_constrain_value (s->opt + option, val, info);
|
|
if (status != SANE_STATUS_GOOD)
|
|
{
|
|
DBG (1, "sane_control_option: sanei_constarin_value failed: %s\n",
|
|
sane_strstatus (status));
|
|
return status;
|
|
}
|
|
if (option >= OPT_TL_X && option <= OPT_BR_Y)
|
|
{
|
|
s->user_corner |= 1 << (option - OPT_TL_X);
|
|
if (-1 == ioctl (s->fd, VIDIOCGWIN, &s->window))
|
|
{
|
|
DBG (1, "sane_control_option: ioctl VIDIOCGWIN failed "
|
|
"(can not get window geometry)\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
s->window.clipcount = 0;
|
|
s->window.clips = 0;
|
|
s->window.height = parms.lines;
|
|
s->window.width = parms.pixels_per_line;
|
|
}
|
|
|
|
|
|
switch (option)
|
|
{
|
|
/* (mostly) side-effect-free word options: */
|
|
case OPT_TL_X:
|
|
break;
|
|
case OPT_TL_Y:
|
|
break;
|
|
case OPT_BR_X:
|
|
s->window.width = *(SANE_Word *) val;
|
|
parms.pixels_per_line = *(SANE_Word *) val;
|
|
if (info)
|
|
*info |= SANE_INFO_RELOAD_PARAMS;
|
|
break;
|
|
case OPT_BR_Y:
|
|
s->window.height = *(SANE_Word *) val;
|
|
parms.lines = *(SANE_Word *) val;
|
|
if (info)
|
|
*info |= SANE_INFO_RELOAD_PARAMS;
|
|
break;
|
|
case OPT_MODE:
|
|
if (info)
|
|
*info |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS;
|
|
s->val[option].s = strdup (val);
|
|
if (!s->val[option].s)
|
|
return SANE_STATUS_NO_MEM;
|
|
if (strcmp (s->val[option].s, "Grey") == 0)
|
|
s->pict.palette = VIDEO_PALETTE_GREY;
|
|
else
|
|
s->pict.palette = VIDEO_PALETTE_RGB24;
|
|
update_parameters (s);
|
|
break;
|
|
case OPT_BRIGHTNESS:
|
|
s->pict.brightness = *(SANE_Word *) val *256;
|
|
s->val[option].w = *(SANE_Word *) val;
|
|
break;
|
|
case OPT_HUE:
|
|
s->pict.hue = *(SANE_Word *) val *256;
|
|
s->val[option].w = *(SANE_Word *) val;
|
|
break;
|
|
case OPT_COLOR:
|
|
s->pict.colour = *(SANE_Word *) val *256;
|
|
s->val[option].w = *(SANE_Word *) val;
|
|
break;
|
|
case OPT_CONTRAST:
|
|
s->pict.contrast = *(SANE_Word *) val *256;
|
|
s->val[option].w = *(SANE_Word *) val;
|
|
break;
|
|
case OPT_WHITE_LEVEL:
|
|
s->pict.whiteness = *(SANE_Word *) val *256;
|
|
s->val[option].w = *(SANE_Word *) val;
|
|
break;
|
|
case OPT_CHANNEL:
|
|
{
|
|
int i;
|
|
struct video_channel channel;
|
|
|
|
s->val[option].s = strdup (val);
|
|
if (!s->val[option].s)
|
|
return SANE_STATUS_NO_MEM;
|
|
for (i = 0; i < MAX_CHANNELS; i++)
|
|
{
|
|
if (strcmp (s->channel[i], val) == 0)
|
|
{
|
|
channel.channel = i;
|
|
if (-1 == ioctl (s->fd, VIDIOCGCHAN, &channel))
|
|
{
|
|
DBG (1, "sane_open: can't ioctl VIDIOCGCHAN %s: %s\n",
|
|
s->devicename, strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
if (-1 == ioctl (s->fd, VIDIOCSCHAN, &channel))
|
|
{
|
|
DBG (1, "sane_open: can't ioctl VIDIOCSCHAN %s: %s\n",
|
|
s->devicename, strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return SANE_STATUS_GOOD;
|
|
break;
|
|
}
|
|
default:
|
|
DBG (1, "sane_control_option: option %d unknown\n", option);
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
if (option >= OPT_TL_X && option <= OPT_BR_Y)
|
|
{
|
|
if (-1 == ioctl (s->fd, VIDIOCSWIN, &s->window))
|
|
{
|
|
DBG (1, "sane_control_option: ioctl VIDIOCSWIN failed (%s)\n",
|
|
strerror (errno));
|
|
/* return SANE_STATUS_INVAL; */
|
|
}
|
|
if (-1 == ioctl (s->fd, VIDIOCGWIN, &s->window))
|
|
{
|
|
DBG (1, "sane_control_option: ioctl VIDIOCGWIN failed (%s)\n",
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
}
|
|
if (option >= OPT_BRIGHTNESS && option <= OPT_WHITE_LEVEL)
|
|
{
|
|
if (-1 == ioctl (s->fd, VIDIOCSPICT, &s->pict))
|
|
{
|
|
DBG (1, "sane_control_option: ioctl VIDIOCSPICT failed (%s)\n",
|
|
strerror (errno));
|
|
/* return SANE_STATUS_INVAL; */
|
|
}
|
|
}
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
else if (action == SANE_ACTION_SET_AUTO)
|
|
{
|
|
if (!(cap & SANE_CAP_AUTOMATIC))
|
|
{
|
|
DBG (1, "sane_control_option: option can't be set automatically\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
switch (option)
|
|
{
|
|
case OPT_BRIGHTNESS:
|
|
/* not implemented yet */
|
|
return SANE_STATUS_GOOD;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_get_parameters (SANE_Handle handle, SANE_Parameters * params)
|
|
{
|
|
V4L_Scanner *s = handle;
|
|
|
|
DBG (4, "sane_get_parameters\n");
|
|
update_parameters (s);
|
|
if (params == 0)
|
|
{
|
|
DBG (1, "sane_get_parameters: params == 0\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
if (-1 == ioctl (s->fd, VIDIOCGWIN, &s->window))
|
|
{
|
|
DBG (1, "sane_control_option: ioctl VIDIOCGWIN failed "
|
|
"(can not get window geometry)\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
parms.pixels_per_line = s->window.width;
|
|
parms.bytes_per_line = s->window.width;
|
|
if (parms.format == SANE_FRAME_RGB)
|
|
parms.bytes_per_line = s->window.width * 3;
|
|
parms.lines = s->window.height;
|
|
*params = parms;
|
|
return SANE_STATUS_GOOD;
|
|
|
|
}
|
|
|
|
SANE_Status
|
|
sane_start (SANE_Handle handle)
|
|
{
|
|
int len;
|
|
V4L_Scanner *s;
|
|
|
|
DBG (2, "sane_start\n");
|
|
for (s = first_handle; s; s = s->next)
|
|
{
|
|
if (s == handle)
|
|
break;
|
|
}
|
|
if (!s)
|
|
{
|
|
DBG (1, "sane_start: bad handle %p\n", handle);
|
|
return SANE_STATUS_INVAL; /* oops, not a handle we know about */
|
|
}
|
|
len = ioctl (s->fd, VIDIOCGCAP, &s->capability);
|
|
if (-1 == len)
|
|
{
|
|
DBG (1, "sane_start: can not get capabilities\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
s->buffercount = 0;
|
|
if (-1 == ioctl (s->fd, VIDIOCGMBUF, &s->mbuf))
|
|
{
|
|
s->is_mmap = SANE_FALSE;
|
|
buffer =
|
|
malloc (s->capability.maxwidth * s->capability.maxheight *
|
|
s->pict.depth);
|
|
if (0 == buffer)
|
|
return SANE_STATUS_NO_MEM;
|
|
DBG (3, "sane_start: V4L trying to read frame\n");
|
|
len = read (s->fd, buffer, parms.bytes_per_line * parms.lines);
|
|
DBG (3, "sane_start: %d bytes read\n", len);
|
|
}
|
|
else
|
|
{
|
|
s->is_mmap = SANE_TRUE;
|
|
DBG (3,
|
|
"sane_start: mmap frame, buffersize: %d bytes, buffers: %d, offset 0 %d\n",
|
|
s->mbuf.size, s->mbuf.frames, s->mbuf.offsets[0]);
|
|
buffer =
|
|
mmap (0, s->mbuf.size, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, 0);
|
|
if ((int) buffer == -1)
|
|
{
|
|
DBG (1, "sane_start: mmap failed: %s\n", strerror (errno));
|
|
return SANE_STATUS_IO_ERROR;
|
|
}
|
|
DBG (3, "sane_start: mmapped frame, capture 1 pict into %p\n", buffer);
|
|
s->mmap.frame = 0;
|
|
s->mmap.width = s->window.width;
|
|
/* s->mmap.width = parms.pixels_per_line; ??? huh? */
|
|
s->mmap.height = s->window.height;
|
|
/* s->mmap.height = parms.lines; ??? huh? */
|
|
s->mmap.format = s->pict.palette;
|
|
DBG (2, "sane_start: mmappeded frame %d x %d with palette %d\n",
|
|
s->mmap.width, s->mmap.height, s->mmap.format);
|
|
len = ioctl (s->fd, VIDIOCMCAPTURE, &s->mmap);
|
|
if (len == -1)
|
|
{
|
|
DBG (1, "sane_start: ioctl VIDIOCMCAPTURE failed: %s\n",
|
|
strerror (errno));
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
DBG (3, "sane_start: waiting for frame %x\n", s->mmap.frame);
|
|
len = ioctl (s->fd, VIDIOCSYNC, &(s->mmap.frame));
|
|
if (-1 == len)
|
|
{
|
|
DBG (1, "sane_start: call to ioctl(%d, VIDIOCSYNC, ..) failed\n",
|
|
s->fd);
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
DBG (3, "sane_start: frame %x done\n", s->mmap.frame);
|
|
}
|
|
DBG (3, "sane_start: done\n");
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_read (SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len,
|
|
SANE_Int * lenp)
|
|
{
|
|
int i, min;
|
|
V4L_Scanner *s = handle;
|
|
|
|
DBG (4, "sane_read: max_len = %d\n", max_len);
|
|
if (!lenp)
|
|
{
|
|
DBG (1, "sane_read: lenp == 0\n");
|
|
return SANE_STATUS_INVAL;
|
|
}
|
|
if ((s->buffercount + 1) > (parms.lines * parms.bytes_per_line))
|
|
{
|
|
*lenp = 0;
|
|
return SANE_STATUS_EOF;
|
|
};
|
|
min = parms.lines * parms.bytes_per_line;
|
|
if (min > (max_len + s->buffercount))
|
|
min = (max_len + s->buffercount);
|
|
if (s->is_mmap == SANE_FALSE)
|
|
{
|
|
for (i = s->buffercount; i < (min + 0); i++)
|
|
{
|
|
*(buf + i - s->buffercount) = *(buffer + i);
|
|
};
|
|
*lenp = (parms.lines * parms.bytes_per_line - s->buffercount);
|
|
if (max_len < *lenp)
|
|
*lenp = max_len;
|
|
DBG (3, "sane_read: tranfered %d bytes (from %d to %d)\n", *lenp,
|
|
s->buffercount, i);
|
|
s->buffercount = i;
|
|
}
|
|
else
|
|
{
|
|
for (i = s->buffercount; i < (min + 0); i++)
|
|
{
|
|
*(buf + i - s->buffercount) = *(buffer + i);
|
|
};
|
|
*lenp = (parms.lines * parms.bytes_per_line - s->buffercount);
|
|
if ((i - s->buffercount) < *lenp)
|
|
*lenp = (i - s->buffercount);
|
|
DBG (3, "sane_read: tranfered %d bytes (from %d to %d)\n", *lenp,
|
|
s->buffercount, i);
|
|
s->buffercount = i;
|
|
}
|
|
return SANE_STATUS_GOOD;
|
|
}
|
|
|
|
void
|
|
sane_cancel (SANE_Handle handle)
|
|
{
|
|
V4L_Scanner *s = handle;
|
|
|
|
DBG (2, "sane_cancel\n");
|
|
/* ??? buffer isn't checked in sane_read? */
|
|
if ((buffer != 0) && (s->is_mmap == SANE_FALSE))
|
|
free (buffer);
|
|
buffer = 0;
|
|
}
|
|
|
|
|
|
SANE_Status
|
|
sane_set_io_mode (SANE_Handle handle, SANE_Bool non_blocking)
|
|
{
|
|
/* Avoid compile warning */
|
|
handle = 0;
|
|
|
|
if (non_blocking == SANE_FALSE)
|
|
return SANE_STATUS_GOOD;
|
|
return SANE_STATUS_UNSUPPORTED;
|
|
}
|
|
|
|
SANE_Status
|
|
sane_get_select_fd (SANE_Handle handle, SANE_Int * fd)
|
|
{
|
|
/* Avoid compile warning */
|
|
handle = 0;
|
|
fd = 0;
|
|
|
|
return SANE_STATUS_UNSUPPORTED;
|
|
}
|