diff options
Diffstat (limited to 'backend/cardscan.c')
-rw-r--r-- | backend/cardscan.c | 1686 |
1 files changed, 1686 insertions, 0 deletions
diff --git a/backend/cardscan.c b/backend/cardscan.c new file mode 100644 index 0000000..6442458 --- /dev/null +++ b/backend/cardscan.c @@ -0,0 +1,1686 @@ +/* sane - Scanner Access Now Easy. + + This file is part of the SANE package, and implements a SANE backend + for various Corex Cardscan scanners. + + Copyright (C) 2007-2010 m. allan noah + + 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 the Corex Cardscan 800C + + The source code is divided in sections which you can easily find by + searching for the tag "@@". + + Section 1 - Init & static stuff + Section 2 - sane_init, _get_devices, _open & friends + Section 3 - sane_*_option functions + Section 4 - sane_start, _get_param, _read & friends + Section 5 - sane_close functions + Section 6 - misc functions + + Changes: + v0, 2007-05-09, MAN (SANE v1.0.19) + - initial release + v1, 2008-02-14, MAN + - sanei_config_read has already cleaned string (#310597) + v2, 2010-02-10, MAN + - add lines_per_block config option + - add has_cal_buffer config option + - basic support for 600c + - clean #include lines + +################################################## + DATA FROM TRACE OF WINDOWS DRIVER: + +cmd packet format: +cmdcode cmdlenlow cmdlenhigh cmdpayloadbytes + +resp packet format: +respcode paperfound resplenlow resplenhigh respayloadbytes + +############ status read loop? ################## +>> 01 01 00 00 +<< 81 00 07 00 00 09 0c 61 c2 7a 0a +>> 34 00 00 +<< b4 00 00 00 +>> 01 01 00 00 +<< 81 00 07 00 00 09 0c 61 c2 7a 0a +>> 34 00 00 +<< b4 00 00 00 +>> 01 01 00 00 +<< 81 00 07 00 00 09 0c 61 c2 7a 0a + +############# scanner settings read? (0x04b8 is scan width) ############# +>> 48 00 00 +<< c8 00 0c 00 b8 04 60 00 00 80 00 00 00 58 ca 7d + +############## color and gray calibration data read ############ +>> 45 00 00 +<< 0x2600 bytes, bbbBBBgggGGGrrrRRRxxxXXX + +############ 34/b4 and 01/81 status loop til paper inserted ############## + +>> 35 01 00 00 +<< b5 01 01 00 00 + +always together? { +>> 14 05 00 80 1b 28 00 0f +<< 94 01 05 00 80 1b 28 00 0f +>> 22 01 00 00 +<< a2 01 01 00 00 +} + +>> 1a 01 00 66 +<< 9a 01 01 00 66 + +>> 19 03 00 51 62 49 +<< 99 01 03 00 51 62 49 + +############# heat up lamp? ################# +===========color=================== +three times { +>> 18 07 00 00 01 60 00 61 00 07 +<< 0x40 read and 0x03 read +the 3 byte drops from f4 f4 f4 to 17 10 08 etc. +} +===========gray=================== +three times { +>> 12 06 00 00 01 60 00 61 00 +<< 0x40 read and 0x01 read +} +the 1 byte drops from f4 to 02 +================================== + +>> 35 01 00 00 +<< b5 01 01 00 00 + +>> 13 01 00 28 +<< 93 01 01 00 28 + +===========color=================== +three times { +>> 18 07 00 01 10 60 00 18 05 07 +<< 0xe2c0 read +} + +14/94 and 22/a2 + +many times { +>> 18 07 00 01 10 60 00 18 05 07 +<< 0xe2c0 read +} +===========gray=================== +two times { +>> 12 06 00 01 10 60 00 18 05 +<< 0x4bc0 read +} + +14/94 and 22/a2 + +many times { +>> 12 06 00 01 10 60 00 18 05 +<< 0x4bc0 read +} +================================== + +>> 35 01 00 ff +<< b5 00 01 00 ff + +14/94 and 22/a2 + +########### discarge capacitor? ########### +four times { +>> 21 02 00 0a 00 +<< a1 00 02 00 0a 00 +} + +>> 01 01 00 00 +<< 81 00 07 00 00 09 0c 61 c2 7a 0a + +>> 35 01 00 ff +<< b5 00 01 00 ff + +>> 34 00 00 +<< b4 00 00 00 +############################################# + + SANE FLOW DIAGRAM + + - sane_init() : initialize backend + . - sane_get_devices() : query list of scanner devices + . - sane_open() : open a particular scanner device + . . - sane_set_io_mode : set blocking mode + . . - sane_get_select_fd : get scanner fd + . . + . . - sane_get_option_descriptor() : get option information + . . - sane_control_option() : change option values + . . - sane_get_parameters() : returns estimated scan parameters + . . - (repeat previous 3 functions) + . . + . . - sane_start() : start image acquisition + . . - sane_get_parameters() : returns actual scan parameters + . . - sane_read() : read image data (from pipe) + . . (sane_read called multiple times; after sane_read returns EOF, + . . loop may continue with sane_start which may return a 2nd page + . . when doing duplex scans, or load the next page from the ADF) + . . + . . - sane_cancel() : cancel operation + . - sane_close() : close opened scanner device + - sane_exit() : terminate use of backend + +*/ + +/* + * @@ Section 1 - Init + */ + +#include "../include/sane/config.h" + +#include <string.h> /*memcpy...*/ +#include <ctype.h> /*isspace*/ + +#include "../include/sane/sanei_backend.h" +#include "../include/sane/sanei_usb.h" +#include "../include/sane/saneopts.h" +#include "../include/sane/sanei_config.h" + +#include "cardscan.h" + +#define DEBUG 1 +#define BUILD 2 + +/* values for SANE_DEBUG_CARDSCAN env var: + - errors 5 + - function trace 10 + - function detail 15 + - get/setopt cmds 20 + - usb cmd trace 25 + - usb cmd detail 30 + - useless noise 35 +*/ + +int global_has_cal_buffer = 1; +int global_lines_per_block = 16; + +/* ------------------------------------------------------------------------- */ +#define STRING_GRAYSCALE SANE_VALUE_SCAN_MODE_GRAY +#define STRING_COLOR SANE_VALUE_SCAN_MODE_COLOR + +/* + * used by attach* and sane_get_devices + * a ptr to a null term array of ptrs to SANE_Device structs + * a ptr to a single-linked list of scanner structs + */ +static const SANE_Device **sane_devArray = NULL; +static struct scanner *scanner_devList = NULL; + +/* + * @@ Section 2 - SANE & scanner init code + */ + +/* + * Called by SANE initially. + * + * From the SANE spec: + * This function must be called before any other SANE function can be + * called. The behavior of a SANE backend is undefined if this + * function is not called first. The version code of the backend is + * returned in the value pointed to by version_code. If that pointer + * is NULL, no version code is returned. Argument authorize is either + * a pointer to a function that is invoked when the backend requires + * authentication for a specific resource or NULL if the frontend does + * not support authentication. + */ +SANE_Status +sane_init (SANE_Int * version_code, SANE_Auth_Callback authorize) +{ + authorize = authorize; /* get rid of compiler warning */ + + DBG_INIT (); + DBG (10, "sane_init: start\n"); + + sanei_usb_init(); + + if (version_code) + *version_code = SANE_VERSION_CODE (SANE_CURRENT_MAJOR, V_MINOR, BUILD); + + DBG (5, "sane_init: cardscan backend %d.%d.%d, from %s\n", + SANE_CURRENT_MAJOR, V_MINOR, BUILD, PACKAGE_STRING); + + DBG (10, "sane_init: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* + * Called by SANE to find out about supported devices. + * + * From the SANE spec: + * This function can be used to query the list of devices that are + * available. If the function executes successfully, it stores a + * pointer to a NULL terminated array of pointers to SANE_Device + * structures in *device_list. The returned list is guaranteed to + * remain unchanged and valid until (a) another call to this function + * is performed or (b) a call to sane_exit() is performed. This + * function can be called repeatedly to detect when new devices become + * available. If argument local_only is true, only local devices are + * returned (devices directly attached to the machine that SANE is + * running on). If it is false, the device list includes all remote + * devices that are accessible to the SANE library. + * + * SANE does not require that this function is called before a + * sane_open() call is performed. A device name may be specified + * explicitly by a user which would make it unnecessary and + * undesirable to call this function first. + * + * Read the config file, find scanners with help from sanei_* + * store in global device structs + */ +SANE_Status +sane_get_devices (const SANE_Device *** device_list, SANE_Bool local_only) +{ + struct scanner *dev; + char line[PATH_MAX]; + const char *lp; + FILE *fp; + int num_devices=0; + int i=0; + + local_only = local_only; /* get rid of compiler warning */ + + DBG (10, "sane_get_devices: start\n"); + + global_has_cal_buffer = 1; + global_lines_per_block = 16; + + fp = sanei_config_open (CONFIG_FILE); + + if (fp) { + + DBG (15, "sane_get_devices: reading config file %s\n", CONFIG_FILE); + + while (sanei_config_read (line, PATH_MAX, fp)) { + + lp = line; + + /* ignore comments */ + if (*lp == '#') + continue; + + /* skip empty lines */ + if (*lp == 0) + continue; + + if ((strncmp ("usb", lp, 3) == 0) && isspace (lp[3])) { + DBG (15, "sane_get_devices: looking for '%s'\n", lp); + sanei_usb_attach_matching_devices(lp, attach_one); + } + + else if (!strncmp(lp, "has_cal_buffer", 14) && isspace (lp[14])) { + + int buf; + lp += 14; + lp = sanei_config_skip_whitespace (lp); + buf = atoi (lp); + + if(buf){ + global_has_cal_buffer = 1; + } + else{ + global_has_cal_buffer = 0; + } + + DBG (15, "sane_get_devices: setting \"has_cal_buffer\" to %d\n", + global_has_cal_buffer); + } + + else if (!strncmp(lp, "lines_per_block", 15) && isspace (lp[15])) { + + int buf; + lp += 15; + lp = sanei_config_skip_whitespace (lp); + buf = atoi (lp); + + if(buf < 1 || buf > 32){ + DBG (15, + "sane_get_devices: \"lines_per_block\"=%d\n out of range", + buf + ); + continue; + } + + DBG (15, "sane_get_devices: \"lines_per_block\" is %d\n", buf); + global_lines_per_block = buf; + } + + else{ + DBG (5, "sane_get_devices: config line \"%s\" ignored.\n", lp); + } + } + fclose (fp); + } + + else { + DBG (5, "sane_get_devices: no config file '%s', using defaults\n", + CONFIG_FILE); + + DBG (15, "sane_get_devices: looking for 'usb 0x08F0 0x0005'\n"); + sanei_usb_attach_matching_devices("usb 0x08F0 0x0005", attach_one); + } + + for (dev = scanner_devList; dev; dev=dev->next) { + DBG (15, "sane_get_devices: found scanner %s\n",dev->device_name); + num_devices++; + } + + DBG (15, "sane_get_devices: found %d scanner(s)\n",num_devices); + + sane_devArray = calloc (num_devices + 1, sizeof (SANE_Device*)); + if (!sane_devArray) + return SANE_STATUS_NO_MEM; + + for (dev = scanner_devList; dev; dev=dev->next) { + sane_devArray[i++] = (SANE_Device *)&dev->sane; + } + + sane_devArray[i] = 0; + + *device_list = sane_devArray; + + DBG (10, "sane_get_devices: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* callback used by sane_get_devices + * build the scanner struct and link to global list + * unless struct is already loaded, then pretend + */ +static SANE_Status +attach_one (const char *device_name) +{ + struct scanner *s; + int ret, i; + SANE_Word vid, pid; + + DBG (10, "attach_one: start '%s'\n", device_name); + + for (s = scanner_devList; s; s = s->next) { + if (strcmp (s->sane.name, device_name) == 0) { + DBG (10, "attach_one: already attached!\n"); + return SANE_STATUS_GOOD; + } + } + + /* build a scanner struct to hold it */ + DBG (15, "attach_one: init struct\n"); + + if ((s = calloc (sizeof (*s), 1)) == NULL) + return SANE_STATUS_NO_MEM; + + /* copy the device name */ + s->device_name = strdup (device_name); + if (!s->device_name){ + free (s); + return SANE_STATUS_NO_MEM; + } + + /* connect the fd */ + DBG (15, "attach_one: connect fd\n"); + + s->fd = -1; + ret = connect_fd(s); + if(ret != SANE_STATUS_GOOD){ + free (s->device_name); + free (s); + return ret; + } + + /* clean up the scanner struct based on model */ + /* this is the only piece of model specific code */ + sanei_usb_get_vendor_product(s->fd,&vid,&pid); + + if(vid == 0x08f0){ + s->vendor_name = "CardScan"; + if(pid == 0x0005){ + s->product_name = "800c"; + } + else if(pid == 0x0002){ + s->product_name = "600c"; + } + else{ + DBG (5, "Unknown product, using default settings\n"); + s->product_name = "Unknown"; + } + } + else{ + DBG (5, "Unknown vendor/product, using default settings\n"); + s->vendor_name = "Unknown"; + s->product_name = "Unknown"; + } + + DBG (15, "attach_one: Found %s scanner %s at %s\n", + s->vendor_name, s->product_name, s->device_name); + + /*copy config file settings*/ + s->has_cal_buffer = global_has_cal_buffer; + s->lines_per_block = global_lines_per_block; + s->color_block_size = s->lines_per_block * PIXELS_PER_LINE * 3; + s->gray_block_size = s->lines_per_block * PIXELS_PER_LINE; + + /* try to get calibration */ + if(s->has_cal_buffer){ + DBG (15, "attach_one: scanner calibration\n"); + + ret = load_calibration(s); + if (ret != SANE_STATUS_GOOD) { + DBG (5, "sane_start: ERROR: cannot calibrate, incompatible?\n"); + free (s->device_name); + free (s); + return ret; + } + } + else{ + DBG (15, "attach_one: skipping calibration\n"); + } + + /* set SANE option 'values' to good defaults */ + DBG (15, "attach_one: init options\n"); + + /* go ahead and setup the first opt, because + * frontend may call control_option on it + * before calling get_option_descriptor + */ + memset (s->opt, 0, sizeof (s->opt)); + for (i = 0; i < NUM_OPTIONS; ++i) { + s->opt[i].name = "filler"; + s->opt[i].size = sizeof (SANE_Word); + s->opt[i].cap = SANE_CAP_INACTIVE; + } + + s->opt[OPT_NUM_OPTS].name = SANE_NAME_NUM_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; + + DBG (15, "attach_one: init settings\n"); + + /* we close the connection, so that another backend can talk to scanner */ + disconnect_fd(s); + + /* load info into sane_device struct */ + s->sane.name = s->device_name; + s->sane.vendor = s->vendor_name; + s->sane.model = s->product_name; + s->sane.type = "scanner"; + + s->next = scanner_devList; + scanner_devList = s; + + DBG (10, "attach_one: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* + * connect the fd in the scanner struct + */ +static SANE_Status +connect_fd (struct scanner *s) +{ + SANE_Status ret; + + DBG (10, "connect_fd: start\n"); + + if(s->fd > -1){ + DBG (5, "connect_fd: already open\n"); + ret = SANE_STATUS_GOOD; + } + else { + DBG (15, "connect_fd: opening USB device\n"); + ret = sanei_usb_open (s->device_name, &(s->fd)); + } + + if(ret != SANE_STATUS_GOOD){ + DBG (5, "connect_fd: could not open device: %d\n", ret); + } + + DBG (10, "connect_fd: finish\n"); + + return ret; +} + +static SANE_Status +load_calibration(struct scanner *s) +{ + SANE_Status ret=SANE_STATUS_GOOD; + unsigned char cmd[] = {0x45, 0x00, 0x00}; + unsigned char * buf; + size_t bytes = HEADER_SIZE + CAL_COLOR_SIZE*2 + CAL_GRAY_SIZE*2; + int j; + + DBG (10, "load_calibration: start\n"); + + buf = malloc(bytes); + if(!buf){ + DBG(5, "load_calibration: not enough mem for buffer: %ld\n",(long)bytes); + return SANE_STATUS_NO_MEM; + } + + ret = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if (ret == SANE_STATUS_GOOD) { + DBG(15, "load_calibration: got GOOD\n"); + + /* + * color cal data comes from scaner like: + * bbbbbbbBBBBBBBgggggggGGGGGGGrrrrrrrRRRRRRR + * where b=darkblue, B=lightblue, etc + * reorder the data into two buffers + * bbbbbbbgggggggrrrrrrr and BBBBBBBGGGGGGGRRRRRRR + */ + + /*dark/light blue*/ + memcpy(s->cal_color_b, buf+HEADER_SIZE, PIXELS_PER_LINE); + memcpy(s->cal_color_w, + buf+HEADER_SIZE+PIXELS_PER_LINE, PIXELS_PER_LINE); + + /*dark/light green*/ + memcpy(s->cal_color_b+PIXELS_PER_LINE, + buf+HEADER_SIZE+(PIXELS_PER_LINE*2), PIXELS_PER_LINE); + memcpy(s->cal_color_w+PIXELS_PER_LINE, + buf+HEADER_SIZE+(PIXELS_PER_LINE*3), PIXELS_PER_LINE); + + /*dark/light red*/ + memcpy(s->cal_color_b+(PIXELS_PER_LINE*2), + buf+HEADER_SIZE+(PIXELS_PER_LINE*4), PIXELS_PER_LINE); + memcpy(s->cal_color_w+(PIXELS_PER_LINE*2), + buf+HEADER_SIZE+(PIXELS_PER_LINE*5), PIXELS_PER_LINE); + + /* then slide the light data down using the dark offset */ + for(j=0;j<CAL_COLOR_SIZE;j++){ + s->cal_color_w[j] -= s->cal_color_b[j]; + } + + /*dark/light gray*/ + memcpy(s->cal_gray_b, + buf+HEADER_SIZE+(CAL_COLOR_SIZE*2), PIXELS_PER_LINE); + memcpy(s->cal_gray_w, + buf+HEADER_SIZE+(CAL_COLOR_SIZE*2)+PIXELS_PER_LINE, PIXELS_PER_LINE); + + /* then slide the light data down using the dark offset */ + for(j=0;j<CAL_GRAY_SIZE;j++){ + s->cal_gray_w[j] -= s->cal_gray_b[j]; + } + + hexdump(35, "cal_color_b:", s->cal_color_b, CAL_COLOR_SIZE); + hexdump(35, "cal_color_w:", s->cal_color_w, CAL_COLOR_SIZE); + hexdump(35, "cal_gray_b:", s->cal_gray_b, CAL_GRAY_SIZE); + hexdump(35, "cal_gray_w:", s->cal_gray_w, CAL_GRAY_SIZE); + } + else { + DBG(5, "load_calibration: error reading data block status = %d\n", ret); + } + + DBG (10, "load_calibration: finish\n"); + + return ret; +} + +/* + * From the SANE spec: + * This function is used to establish a connection to a particular + * device. The name of the device to be opened is passed in argument + * name. If the call completes successfully, a handle for the device + * is returned in *h. As a special case, specifying a zero-length + * string as the device requests opening the first available device + * (if there is such a device). + */ +SANE_Status +sane_open (SANE_String_Const name, SANE_Handle * handle) +{ + struct scanner *dev = NULL; + struct scanner *s = NULL; + SANE_Status ret; + + DBG (10, "sane_open: start\n"); + + if(name[0] == 0){ + if(scanner_devList){ + DBG (15, "sane_open: no device requested, using first\n"); + s = scanner_devList; + } + else{ + DBG (15, "sane_open: no device requested, none found\n"); + } + } + else{ + DBG (15, "sane_open: device %s requested, attaching\n", name); + + ret = attach_one(name); + if(ret){ + DBG (5, "sane_open: attach error %d\n",ret); + return ret; + } + + for (dev = scanner_devList; dev; dev = dev->next) { + if (strcmp (dev->sane.name, name) == 0) { + s = dev; + break; + } + } + } + + if (!s) { + DBG (5, "sane_open: no device found\n"); + return SANE_STATUS_INVAL; + } + + DBG (15, "sane_open: device %s found\n", s->sane.name); + + *handle = s; + + /* connect the fd so we can talk to scanner */ + ret = connect_fd(s); + if(ret != SANE_STATUS_GOOD){ + return ret; + } + + DBG (10, "sane_open: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* + * @@ Section 3 - SANE Options functions + */ + +/* + * Returns the options we know. + * + * From the SANE spec: + * This function is used to access option descriptors. The function + * returns the option descriptor for option number n of the device + * represented by handle h. Option number 0 is guaranteed to be a + * valid option. Its value is an integer that specifies the number of + * options that are available for device handle h (the count includes + * option 0). If n is not a valid option index, the function returns + * NULL. The returned option descriptor is guaranteed to remain valid + * (and at the returned address) until the device is closed. + */ +const SANE_Option_Descriptor * +sane_get_option_descriptor (SANE_Handle handle, SANE_Int option) +{ + struct scanner *s = handle; + int i; + SANE_Option_Descriptor *opt = &s->opt[option]; + + DBG (20, "sane_get_option_descriptor: %d\n", option); + + if ((unsigned) option >= NUM_OPTIONS) + return NULL; + + /* "Mode" group -------------------------------------------------------- */ + if(option==OPT_MODE_GROUP){ + opt->title = "Scan Mode"; + opt->desc = ""; + opt->type = SANE_TYPE_GROUP; + opt->constraint_type = SANE_CONSTRAINT_NONE; + } + + /* scan mode */ + else if(option==OPT_MODE){ + i=0; + s->mode_list[i++]=STRING_GRAYSCALE; + s->mode_list[i++]=STRING_COLOR; + s->mode_list[i]=NULL; + + opt->name = SANE_NAME_SCAN_MODE; + opt->title = SANE_TITLE_SCAN_MODE; + opt->desc = SANE_DESC_SCAN_MODE; + opt->type = SANE_TYPE_STRING; + opt->constraint_type = SANE_CONSTRAINT_STRING_LIST; + opt->constraint.string_list = s->mode_list; + opt->size = maxStringSize (opt->constraint.string_list); + opt->cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; + } + + return opt; +} + +/** + * Gets or sets an option value. + * + * From the SANE spec: + * This function is used to set or inquire the current value of option + * number n of the device represented by handle h. The manner in which + * the option is controlled is specified by parameter action. The + * possible values of this parameter are described in more detail + * below. The value of the option is passed through argument val. It + * is a pointer to the memory that holds the option value. The memory + * area pointed to by v must be big enough to hold the entire option + * value (determined by member size in the corresponding option + * descriptor). + * + * The only exception to this rule is that when setting the value of a + * string option, the string pointed to by argument v may be shorter + * since the backend will stop reading the option value upon + * encountering the first NUL terminator in the string. If argument i + * is not NULL, the value of *i will be set to provide details on how + * well the request has been met. + */ +SANE_Status +sane_control_option (SANE_Handle handle, SANE_Int option, + SANE_Action action, void *val, SANE_Int * info) +{ + struct scanner *s = (struct scanner *) handle; + SANE_Int dummy = 0; + + /* Make sure that all those statements involving *info cannot break (better + * than having to do "if (info) ..." everywhere!) + */ + if (info == 0) + info = &dummy; + + if (option >= NUM_OPTIONS) { + DBG (5, "sane_control_option: %d too big\n", option); + return SANE_STATUS_INVAL; + } + + if (!SANE_OPTION_IS_ACTIVE (s->opt[option].cap)) { + DBG (5, "sane_control_option: %d inactive\n", option); + return SANE_STATUS_INVAL; + } + + /* + * SANE_ACTION_GET_VALUE: We have to find out the current setting and + * return it in a human-readable form (often, text). + */ + if (action == SANE_ACTION_GET_VALUE) { + SANE_Word * val_p = (SANE_Word *) val; + + DBG (20, "sane_control_option: get value for '%s' (%d)\n", s->opt[option].name,option); + + switch (option) { + + case OPT_NUM_OPTS: + *val_p = NUM_OPTIONS; + return SANE_STATUS_GOOD; + + case OPT_MODE: + if(s->mode == MODE_GRAYSCALE){ + strcpy (val, STRING_GRAYSCALE); + } + else if(s->mode == MODE_COLOR){ + strcpy (val, STRING_COLOR); + } + return SANE_STATUS_GOOD; + } + } + else if (action == SANE_ACTION_SET_VALUE) { + int tmp; + SANE_Word val_c; + SANE_Status status; + + DBG (20, "sane_control_option: set value for '%s' (%d)\n", s->opt[option].name,option); + + if ( s->started ) { + DBG (5, "sane_control_option: cant set, device busy\n"); + return SANE_STATUS_DEVICE_BUSY; + } + + if (!SANE_OPTION_IS_SETTABLE (s->opt[option].cap)) { + DBG (5, "sane_control_option: not settable\n"); + return SANE_STATUS_INVAL; + } + + status = sanei_constrain_value (s->opt + option, val, info); + if (status != SANE_STATUS_GOOD) { + DBG (5, "sane_control_option: bad value\n"); + return status; + } + + /* may have been changed by constrain, so dont copy until now */ + val_c = *(SANE_Word *)val; + + /* + * Note - for those options which can assume one of a list of + * valid values, we can safely assume that they will have + * exactly one of those values because that's what + * sanei_constrain_value does. Hence no "else: invalid" branches + * below. + */ + switch (option) { + + /* Mode Group */ + case OPT_MODE: + if (!strcmp (val, STRING_GRAYSCALE)) { + tmp = MODE_GRAYSCALE; + } + else{ + tmp = MODE_COLOR; + } + + if (tmp == s->mode) + return SANE_STATUS_GOOD; + + s->mode = tmp; + *info |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + return SANE_STATUS_GOOD; + + } /* switch */ + } /* else */ + + return SANE_STATUS_INVAL; +} + +/* + * @@ Section 4 - SANE scanning functions + */ +/* + * Called by SANE to retrieve information about the type of data + * that the current scan will return. + * + * From the SANE spec: + * This function is used to obtain the current scan parameters. The + * returned parameters are guaranteed to be accurate between the time + * a scan has been started (sane_start() has been called) and the + * completion of that request. Outside of that window, the returned + * values are best-effort estimates of what the parameters will be + * when sane_start() gets invoked. + * + * Calling this function before a scan has actually started allows, + * for example, to get an estimate of how big the scanned image will + * be. The parameters passed to this function are the handle h of the + * device for which the parameters should be obtained and a pointer p + * to a parameter structure. + */ +SANE_Status +sane_get_parameters (SANE_Handle handle, SANE_Parameters * params) +{ + struct scanner *s = (struct scanner *) handle; + + DBG (10, "sane_get_parameters: start\n"); + + params->pixels_per_line = PIXELS_PER_LINE; + params->lines = -1; + params->last_frame = 1; + + if (s->mode == MODE_COLOR) { + params->format = SANE_FRAME_RGB; + params->depth = 8; + params->bytes_per_line = params->pixels_per_line * 3; + } + else if (s->mode == MODE_GRAYSCALE) { + params->format = SANE_FRAME_GRAY; + params->depth = 8; + params->bytes_per_line = params->pixels_per_line; + } + + DBG (15, "\tdepth %d\n", params->depth); + DBG (15, "\tlines %d\n", params->lines); + DBG (15, "\tpixels_per_line %d\n", params->pixels_per_line); + DBG (15, "\tbytes_per_line %d\n", params->bytes_per_line); + + DBG (10, "sane_get_parameters: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* + * Called by SANE when a page acquisition operation is to be started. + */ +SANE_Status +sane_start (SANE_Handle handle) +{ + struct scanner *s = handle; + SANE_Status ret; + + DBG (10, "sane_start: start\n"); + + /* first page of batch */ + if(s->started){ + DBG(5,"sane_start: previous transfer not finished?"); + sane_cancel((SANE_Handle)s); + return SANE_STATUS_CANCELLED; + } + + /* set clean defaults */ + s->started=1; + s->bytes_rx=0; + s->bytes_tx=0; + s->paperless_lines=0; + + /* heat up the lamp */ + if(s->mode == MODE_COLOR){ + ret = heat_lamp_color(s); + } + else{ + ret = heat_lamp_gray(s); + } + + if (ret != SANE_STATUS_GOOD) { + DBG (5, "sane_start: ERROR: failed to heat lamp\n"); + sane_cancel((SANE_Handle)s); + return ret; + } + + DBG (10, "sane_start: finish\n"); + + return SANE_STATUS_GOOD; +} + +static SANE_Status +heat_lamp_gray(struct scanner *s) +{ + SANE_Status ret = SANE_STATUS_GOOD; + SANE_Status ret2 = SANE_STATUS_GOOD; + unsigned char cmd[] = + {0x12, 0x06, 0x00, 0x00, 0x01, 0x60, 0x00, 0x61, 0x00}; + size_t bytes = HEADER_SIZE + 1; + unsigned char * buf; + int i; + + DBG (10, "heat_lamp_gray: start\n"); + + buf = malloc(bytes); + if(!buf){ + DBG(5, "heat_lamp_gray: not enough mem for buffer: %lu\n", + (long unsigned)bytes); + return SANE_STATUS_NO_MEM; + } + + for(i=0;i<10;i++){ + + ret2 = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if (ret2 != SANE_STATUS_GOOD) { + DBG(5, "heat_lamp_gray: %d error\n",i); + ret = ret2; + break; + } + + if(!buf[1]){ + DBG(5, "heat_lamp_gray: %d got no docs\n",i); + ret = SANE_STATUS_NO_DOCS; + break; + } + + DBG(15, "heat_lamp_gray: %d got: %d %d\n",i, + buf[HEADER_SIZE],s->cal_gray_b[0]); + + if(buf[HEADER_SIZE] < 0x20){ + DBG(15, "heat_lamp_gray: hot\n"); + ret = SANE_STATUS_GOOD; + break; + } + else{ + DBG(15, "heat_lamp_gray: cold\n"); + ret = SANE_STATUS_DEVICE_BUSY; + } + } + + free(buf); + + DBG (10, "heat_lamp_gray: finish %d\n",ret); + + return ret; +} + +static SANE_Status +heat_lamp_color(struct scanner *s) +{ + SANE_Status ret = SANE_STATUS_GOOD; + SANE_Status ret2 = SANE_STATUS_GOOD; + unsigned char cmd[] = + {0x18, 0x07, 0x00, 0x00, 0x01, 0x60, 0x00, 0x61, 0x00, 0x07}; + size_t bytes = HEADER_SIZE + 3; + unsigned char * buf; + int i; + + DBG (10, "heat_lamp_color: start\n"); + + buf = malloc(bytes); + if(!buf){ + DBG(5, "heat_lamp_color: not enough mem for buffer: %lu\n", + (long unsigned)bytes); + return SANE_STATUS_NO_MEM; + } + + for(i=0;i<10;i++){ + + ret2 = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if (ret2 != SANE_STATUS_GOOD) { + DBG(5, "heat_lamp_color: %d error\n",i); + ret = ret2; + break; + } + + if(!buf[1]){ + DBG(5, "heat_lamp_color: %d got no docs\n",i); + ret = SANE_STATUS_NO_DOCS; + break; + } + + DBG(15, "heat_lamp_color: %d got: %d,%d,%d %d,%d,%d\n",i, + buf[HEADER_SIZE],buf[HEADER_SIZE+1],buf[HEADER_SIZE+2], + s->cal_color_b[0],s->cal_color_b[1],s->cal_color_b[2]); + + if(buf[HEADER_SIZE] < 0x20 + && buf[HEADER_SIZE+1] < 0x20 + && buf[HEADER_SIZE+2] < 0x20){ + DBG(15, "heat_lamp_color: hot\n"); + ret = SANE_STATUS_GOOD; + break; + } + else{ + DBG(15, "heat_lamp_color: cold\n"); + ret = SANE_STATUS_DEVICE_BUSY; + } + } + + free(buf); + + DBG (10, "heat_lamp_color: finish %d\n",ret); + + return ret; +} + +/* + * Called by SANE to read data. + * + * From the SANE spec: + * This function is used to read image data from the device + * represented by handle h. Argument buf is a pointer to a memory + * area that is at least maxlen bytes long. The number of bytes + * returned is stored in *len. A backend must set this to zero when + * the call fails (i.e., when a status other than SANE_STATUS_GOOD is + * returned). + * + * When the call succeeds, the number of bytes returned can be + * anywhere in the range from 0 to maxlen bytes. + */ +SANE_Status +sane_read (SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len, SANE_Int * len) +{ + struct scanner *s = (struct scanner *) handle; + SANE_Status ret=SANE_STATUS_GOOD; + + DBG (10, "sane_read: start\n"); + + *len = 0; + + /* cancelled? */ + if(!s->started){ + DBG (5, "sane_read: call sane_start first\n"); + return SANE_STATUS_CANCELLED; + } + + /* have sent all of current buffer */ + if(s->bytes_tx == s->bytes_rx){ + + /* at end of data, stop */ + if(s->paperless_lines >= MAX_PAPERLESS_LINES){ + DBG (15, "sane_read: returning eof\n"); + power_down(s); + return SANE_STATUS_EOF; + } + + /* more to get, reset and go */ + s->bytes_tx = 0; + s->bytes_rx = 0; + + if(s->mode == MODE_COLOR){ + ret = read_from_scanner_color(s); + } + else{ + ret = read_from_scanner_gray(s); + } + + if(ret){ + DBG(5,"sane_read: returning %d\n",ret); + return ret; + } + } + + /* data in current buffer, send some of it */ + *len = s->bytes_rx - s->bytes_tx; + if(*len > max_len){ + *len = max_len; + } + + memcpy(buf,s->buffer+s->bytes_tx,*len); + s->bytes_tx += *len; + + DBG (10, "sane_read: %d,%d,%d finish\n", *len,s->bytes_rx,s->bytes_tx); + + return ret; +} + +static SANE_Status +read_from_scanner_gray(struct scanner *s) +{ + SANE_Status ret=SANE_STATUS_GOOD; + /*cmd len-le16 move lines ??? ??? ??? ???*/ + unsigned char cmd[] = + {0x12, 0x06, 0x00, 0x01, 0x01, 0x60, 0x00, 0x18, 0x05}; + size_t bytes = HEADER_SIZE + s->gray_block_size; + unsigned char * buf; + int i,j; + + DBG (10, "read_from_scanner_gray: start\n"); + + cmd[4] = s->lines_per_block; + + buf = malloc(bytes); + if(!buf){ + DBG(5, "read_from_scanner_gray: not enough mem for buffer: %lu\n", + (long unsigned)bytes); + return SANE_STATUS_NO_MEM; + } + + ret = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if (ret == SANE_STATUS_GOOD) { + + DBG(15, "read_from_scanner_gray: got GOOD\n"); + + if(!buf[1]){ + s->paperless_lines += s->lines_per_block; + } + + s->bytes_rx = s->gray_block_size; + + /*memcpy(s->buffer,buf+HEADER_SIZE,s->gray_block_size);*/ + + /* reorder the gray data into the struct's buffer */ + for(i=0;i<s->gray_block_size;i+=PIXELS_PER_LINE){ + for(j=0;j<PIXELS_PER_LINE;j++){ + + unsigned char byte = buf[ HEADER_SIZE + i + j ]; + unsigned char bcal = s->cal_gray_b[j]; + unsigned char wcal = s->cal_gray_w[j]; + + byte = (byte <= bcal)?0:(byte-bcal); + byte = (byte >= wcal)?255:(byte*255/wcal); + s->buffer[i+j] = byte; + } + } + } + else { + DBG(5, "read_from_scanner_gray: error reading status = %d\n", ret); + } + + free(buf); + + DBG (10, "read_from_scanner_gray: finish\n"); + + return ret; +} + +static SANE_Status +read_from_scanner_color(struct scanner *s) +{ + SANE_Status ret=SANE_STATUS_GOOD; + unsigned char cmd[] = + {0x18, 0x07, 0x00, 0x01, 0x01, 0x60, 0x00, 0x18, 0x05, 0x07}; + size_t bytes = HEADER_SIZE + s->color_block_size; + unsigned char * buf; + int i,j,k; + + DBG (10, "read_from_scanner_color: start\n"); + + cmd[4] = s->lines_per_block; + + buf = malloc(bytes); + if(!buf){ + DBG(5, "read_from_scanner_color: not enough mem for buffer: %lu\n", + (long unsigned)bytes); + return SANE_STATUS_NO_MEM; + } + + ret = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if (ret == SANE_STATUS_GOOD) { + + DBG(15, "read_from_scanner_color: got GOOD\n"); + + if(!buf[1]){ + s->paperless_lines += s->lines_per_block; + } + + s->bytes_rx = s->color_block_size; + + /*memcpy(s->buffer,buf+HEADER_SIZE,s->color_block_size);*/ + + /* reorder the color data into the struct's buffer */ + for(i=0;i<s->color_block_size;i+=PIXELS_PER_LINE*3){ + for(j=0;j<PIXELS_PER_LINE;j++){ + for(k=0;k<3;k++){ + + int offset = PIXELS_PER_LINE*(2-k) + j; + unsigned char byte = buf[ HEADER_SIZE + i + offset ]; + unsigned char bcal = s->cal_color_b[offset]; + unsigned char wcal = s->cal_color_w[offset]; + + byte = (byte <= bcal)?0:(byte-bcal); + byte = (byte >= wcal)?255:(byte*255/wcal); + s->buffer[i+j*3+k] = byte; + } + } + } + } + else { + DBG(5, "read_from_scanner_color: error reading status = %d\n", ret); + } + + free(buf); + + DBG (10, "read_from_scanner_color: finish\n"); + + return ret; +} + +/* + * @@ Section 4 - SANE cleanup functions + */ +/* + * Cancels a scan. + * + * From the SANE spec: + * This function is used to immediately or as quickly as possible + * cancel the currently pending operation of the device represented by + * handle h. This function can be called at any time (as long as + * handle h is a valid handle) but usually affects long-running + * operations only (such as image is acquisition). It is safe to call + * this function asynchronously (e.g., from within a signal handler). + * It is important to note that completion of this operaton does not + * imply that the currently pending operation has been cancelled. It + * only guarantees that cancellation has been initiated. Cancellation + * completes only when the cancelled call returns (typically with a + * status value of SANE_STATUS_CANCELLED). Since the SANE API does + * not require any other operations to be re-entrant, this implies + * that a frontend must not call any other operation until the + * cancelled operation has returned. + */ +void +sane_cancel (SANE_Handle handle) +{ + struct scanner * s = (struct scanner *) handle; + DBG (10, "sane_cancel: start\n"); + s->started = 0; + DBG (10, "sane_cancel: finish\n"); +} + +static SANE_Status +power_down(struct scanner *s) +{ + SANE_Status ret = SANE_STATUS_GOOD; + + unsigned char cmd[] = {0x21, 0x02, 0x00, 0x0a, 0x00}; + unsigned char buf[6]; + size_t bytes = sizeof(buf); + int i; + + DBG (10, "power_down: start\n"); + + for(i=0;i<5;i++){ + ret = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); + + if(ret != SANE_STATUS_GOOD){ + break; + } + } + +#if 0 + unsigned char cmd[] = {0x35, 0x01, 0x00, 0xff}; + unsigned char buf[5]; + size_t bytes = sizeof(buf); + + DBG (10, "power_down: start\n"); + + ret = do_cmd( + s, 0, + cmd, sizeof(cmd), + NULL, 0, + buf, &bytes + ); +#endif + + DBG (10, "power_down: finish %d\n",ret); + + return ret; +} + +/* + * Ends use of the scanner. + * + * From the SANE spec: + * This function terminates the association between the device handle + * passed in argument h and the device it represents. If the device is + * presently active, a call to sane_cancel() is performed first. After + * this function returns, handle h must not be used anymore. + */ +void +sane_close (SANE_Handle handle) +{ + DBG (10, "sane_close: start\n"); + + sane_cancel(handle); + disconnect_fd((struct scanner *) handle); + + DBG (10, "sane_close: finish\n"); +} + +static SANE_Status +disconnect_fd (struct scanner *s) +{ + DBG (10, "disconnect_fd: start\n"); + + if(s->fd > -1){ + DBG (15, "disconnecting usb device\n"); + sanei_usb_close (s->fd); + s->fd = -1; + } + + DBG (10, "disconnect_fd: finish\n"); + + return SANE_STATUS_GOOD; +} + +/* + * Terminates the backend. + * + * From the SANE spec: + * This function must be called to terminate use of a backend. The + * function will first close all device handles that still might be + * open (it is recommended to close device handles explicitly through + * a call to sane_close(), but backends are required to release all + * resources upon a call to this function). After this function + * returns, no function other than sane_init() may be called + * (regardless of the status value returned by sane_exit(). Neglecting + * to call this function may result in some resources not being + * released properly. + */ +void +sane_exit (void) +{ + struct scanner *dev, *next; + + DBG (10, "sane_exit: start\n"); + + for (dev = scanner_devList; dev; dev = next) { + disconnect_fd(dev); + next = dev->next; + free (dev->device_name); + free (dev); + } + + if (sane_devArray) + free (sane_devArray); + + scanner_devList = NULL; + sane_devArray = NULL; + + DBG (10, "sane_exit: finish\n"); +} + + +/* + * @@ Section 5 - misc helper functions + */ +/* + * take a bunch of pointers, send commands to scanner + */ +static SANE_Status +do_cmd(struct scanner *s, int shortTime, + unsigned char * cmdBuff, size_t cmdLen, + unsigned char * outBuff, size_t outLen, + unsigned char * inBuff, size_t * inLen +) +{ + /* sanei_usb overwrites the transfer size, so make some local copies */ + size_t loc_cmdLen = cmdLen; + size_t loc_outLen = outLen; + size_t loc_inLen = *inLen; + + int cmdTime = USB_COMMAND_TIME; + int outTime = USB_DATA_TIME; + int inTime = USB_DATA_TIME; + + int ret = 0; + + DBG (10, "do_cmd: start\n"); + + if(shortTime){ + cmdTime /= 20; + outTime /= 20; + inTime /= 20; + } + + /* change timeout */ + sanei_usb_set_timeout(cmdTime); + + /* write the command out */ + DBG(25, "cmd: writing %ld bytes, timeout %d\n", (long)cmdLen, cmdTime); + hexdump(30, "cmd: >>", cmdBuff, cmdLen); + ret = sanei_usb_write_bulk(s->fd, cmdBuff, &cmdLen); + DBG(25, "cmd: wrote %ld bytes, retVal %d\n", (long)cmdLen, ret); + + if(ret == SANE_STATUS_EOF){ + DBG(5,"cmd: got EOF, returning IO_ERROR\n"); + return SANE_STATUS_IO_ERROR; + } + if(ret != SANE_STATUS_GOOD){ + DBG(5,"cmd: return error '%s'\n",sane_strstatus(ret)); + return ret; + } + if(loc_cmdLen != cmdLen){ + DBG(5,"cmd: wrong size %ld/%ld\n", (long)loc_cmdLen, (long)cmdLen); + return SANE_STATUS_IO_ERROR; + } + + /* this command has a write component, and a place to get it */ + if(outBuff && outLen && outTime){ + + /* change timeout */ + sanei_usb_set_timeout(outTime); + + DBG(25, "out: writing %ld bytes, timeout %d\n", (long)outLen, outTime); + hexdump(30, "out: >>", outBuff, outLen); + ret = sanei_usb_write_bulk(s->fd, outBuff, &outLen); + DBG(25, "out: wrote %ld bytes, retVal %d\n", (long)outLen, ret); + + if(ret == SANE_STATUS_EOF){ + DBG(5,"out: got EOF, returning IO_ERROR\n"); + return SANE_STATUS_IO_ERROR; + } + if(ret != SANE_STATUS_GOOD){ + DBG(5,"out: return error '%s'\n",sane_strstatus(ret)); + return ret; + } + if(loc_outLen != outLen){ + DBG(5,"out: wrong size %ld/%ld\n", (long)loc_outLen, (long)outLen); + return SANE_STATUS_IO_ERROR; + } + } + + /* this command has a read component, and a place to put it */ + if(inBuff && inLen && inTime){ + + memset(inBuff,0,*inLen); + + /* change timeout */ + sanei_usb_set_timeout(inTime); + + DBG(25, "in: reading %ld bytes, timeout %d\n", (long)*inLen, inTime); + ret = sanei_usb_read_bulk(s->fd, inBuff, inLen); + DBG(25, "in: retVal %d\n", ret); + + if(ret == SANE_STATUS_EOF){ + DBG(5,"in: got EOF, continuing\n"); + } + else if(ret != SANE_STATUS_GOOD){ + DBG(5,"in: return error '%s'\n",sane_strstatus(ret)); + return ret; + } + + DBG(25, "in: read %ld bytes\n", (long)*inLen); + if(*inLen){ + hexdump(30, "in: <<", inBuff, *inLen); + } + + if(loc_inLen != *inLen){ + ret = SANE_STATUS_EOF; + DBG(5,"in: short read %ld/%ld\n", (long)loc_inLen, (long)*inLen); + } + } + + DBG (10, "do_cmd: finish\n"); + + return ret; +} + +/** + * Convenience method to determine longest string size in a list. + */ +static size_t +maxStringSize (const SANE_String_Const strings[]) +{ + size_t size, max_size = 0; + int i; + + for (i = 0; strings[i]; ++i) { + size = strlen (strings[i]) + 1; + if (size > max_size) + max_size = size; + } + + return max_size; +} + +/** + * Prints a hex dump of the given buffer onto the debug output stream. + */ +static void +hexdump (int level, char *comment, unsigned char *p, int l) +{ + int i; + char line[128]; + char *ptr; + + if(DBG_LEVEL < level) + return; + + DBG (level, "%s\n", comment); + ptr = line; + for (i = 0; i < l; i++, p++) + { + if ((i % 16) == 0) + { + if (ptr != line) + { + *ptr = '\0'; + DBG (level, "%s\n", line); + ptr = line; + } + sprintf (ptr, "%3.3x:", i); + ptr += 4; + } + sprintf (ptr, " %2.2x", *p); + ptr += 3; + } + *ptr = '\0'; + DBG (level, "%s\n", line); +} + +/** + * An advanced method we don't support but have to define. + */ +SANE_Status +sane_set_io_mode (SANE_Handle h, SANE_Bool non_blocking) +{ + DBG (10, "sane_set_io_mode\n"); + DBG (15, "%d %p\n", non_blocking, h); + return SANE_STATUS_UNSUPPORTED; +} + +/** + * An advanced method we don't support but have to define. + */ +SANE_Status +sane_get_select_fd (SANE_Handle h, SANE_Int *fdp) +{ + DBG (10, "sane_get_select_fd\n"); + DBG (15, "%p %d\n", h, *fdp); + return SANE_STATUS_UNSUPPORTED; +} |