diff --git a/.editorconfig b/.editorconfig index 0faa31d3a..107f9eca1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,7 @@ root = true ; look no further charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true + +[backend/escl/*] +indent_size = 4 +indent_style = space diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2c992a91b..603ced594 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ variables: REGISTRY_HUB: "registry.gitlab.com/sane-project/ci-envs" CONFIGURE_MINI: "--enable-silent-rules" - CONFIGURE_FULL: "--with-usb --enable-avahi --enable-pnm-backend" + CONFIGURE_FULL: "--with-usb --enable-avahi --enable-pnm-backend --with-libcurl" stages: - tarball diff --git a/AUTHORS b/AUTHORS index 783c881d3..bb2bbfea4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Backends: epson: Karl Heinz Kremer epson2: Alessandro Zummo epsonds: Alessandro Zummo + escl: Touboul Nathane, Thierry HUCHARD (*) fujitsu: Randolph Bentson, Frederik Ramm, Oliver Schirrmeister, m. allan noah (*) genesys: Henning Geinitz, Gerhard Jaeger (*), Stéphane Voltz, @@ -248,9 +249,11 @@ Sergey Vlasov Simon Krix Simon Munton Stéphane Voltz +Thierry HUCHARD Thomas Soumarmon Tom Martone Tom Wang +Touboul Nathane Tristan Tarrant Troy Rollo Ullrich Sigwanz diff --git a/acinclude.m4 b/acinclude.m4 index 1bc3b7475..f5bd84bca 100644 --- a/acinclude.m4 +++ b/acinclude.m4 @@ -611,6 +611,26 @@ for be in ${BACKENDS}; do fi ;; + escl) + if test "x${enable_avahi}" != "xyes"; then + echo "*** $be backend requires AVAHI library - $DISABLE_MSG" + backend_supported="no" + fi + if test "x${with_libcurl}" != "xyes"; then + echo "*** $be backend requires cURL library - $DISABLE_MSG" + backend_supported="no" + fi + if test "x${have_libxml}" != "xyes"; then + echo "*** $be backend requires XML library - $DISABLE_MSG" + backend_supported="no" + fi + # FIXME: Remove when PNG and/or PDF support have been added. + if test "x${sane_cv_use_libjpeg}" != "xyes"; then + echo "*** $be backend currently requires JPEG library - $DISABLE_MSG" + backend_supported="no" + fi + ;; + gphoto2) if test "${HAVE_GPHOTO2}" != "true" \ || test "${sane_cv_use_libjpeg}" != "yes"; then diff --git a/backend/Makefile.am b/backend/Makefile.am index c2de3f5dc..7a7354782 100644 --- a/backend/Makefile.am +++ b/backend/Makefile.am @@ -68,8 +68,8 @@ BACKEND_CONFS= abaton.conf agfafocus.conf apple.conf artec.conf \ canon_pp.conf cardscan.conf coolscan2.conf coolscan3.conf \ coolscan.conf dc210.conf dc240.conf dc25.conf \ dell1600n_net.conf dmc.conf epjitsu.conf epson2.conf \ - epson.conf epsonds.conf fujitsu.conf genesys.conf gphoto2.conf \ - gt68xx.conf hp3900.conf hp4200.conf hp5400.conf \ + epson.conf epsonds.conf escl.conf fujitsu.conf genesys.conf \ + gphoto2.conf gt68xx.conf hp3900.conf hp4200.conf hp5400.conf \ hp.conf hpsj5s.conf hs2p.conf ibm.conf kodak.conf kodakaio.conf\ kvs1025.conf \ leo.conf lexmark.conf ma1509.conf magicolor.conf \ @@ -161,7 +161,7 @@ be_convenience_libs = libabaton.la libagfafocus.la \ libcoolscan2.la libcoolscan3.la libdc25.la \ libdc210.la libdc240.la libdell1600n_net.la \ libdmc.la libdll.la libdll_preload.la libepjitsu.la libepson.la \ - libepson2.la libepsonds.la libfujitsu.la libgenesys.la \ + libepson2.la libepsonds.la libescl.la libfujitsu.la libgenesys.la \ libgphoto2_i.la libgt68xx.la libhp.la \ libhp3500.la libhp3900.la libhp4200.la \ libhp5400.la libhp5590.la libhpljm1005.la \ @@ -194,8 +194,8 @@ be_dlopen_libs = libsane-abaton.la libsane-agfafocus.la \ libsane-coolscan2.la libsane-coolscan3.la libsane-dc25.la \ libsane-dc210.la libsane-dc240.la libsane-dell1600n_net.la \ libsane-dmc.la libsane-epjitsu.la libsane-epson.la \ - libsane-epson2.la libsane-epsonds.la libsane-fujitsu.la libsane-genesys.la \ - libsane-gphoto2.la libsane-gt68xx.la libsane-hp.la \ + libsane-epson2.la libsane-epsonds.la libsane-escl.la libsane-fujitsu.la \ + libsane-genesys.la libsane-gphoto2.la libsane-gt68xx.la libsane-hp.la \ libsane-hp3500.la libsane-hp3900.la libsane-hp4200.la \ libsane-hp5400.la libsane-hp5590.la libsane-hpljm1005.la \ libsane-hpsj5s.la libsane-hs2p.la libsane-ibm.la libsane-kodak.la libsane-kodakaio.la\ @@ -434,6 +434,21 @@ libsane_dmc_la_LDFLAGS = $(DIST_SANELIBS_LDFLAGS) libsane_dmc_la_LIBADD = $(COMMON_LIBS) libdmc.la ../sanei/sanei_init_debug.lo ../sanei/sanei_constrain_value.lo ../sanei/sanei_config.lo ../sanei/sanei_config2.lo sane_strstatus.lo ../sanei/sanei_scsi.lo $(SCSI_LIBS) $(RESMGR_LIBS) EXTRA_DIST += dmc.conf.in +if have_libavahi +if have_libcurl +if have_libxml2 +libescl_la_SOURCES = escl/escl.c escl/escl_capabilities.c escl/escl_devices.c escl/escl.h escl/escl_newjob.c escl/escl_reset.c escl/escl_scan.c escl/escl_status.c +libescl_la_CPPFLAGS = $(AM_CPPFLAGS) $(JPEG_CFLAGS) $(XML_CFLAGS) $(libcurl_CFLAGS) $(AVAHI_CFLAGS) -DBACKEND_NAME=escl + +nodist_libsane_escl_la_SOURCES = escl-s.c +libsane_escl_la_CPPFLAGS = $(AM_CPPFLAGS) $(JPEG_CFLAGS) $(XML_CFLAGS) $(libcurl_CFLAGS) $(AVAHI_CFLAGS) -DBACKEND_NAME=escl +libsane_escl_la_LDFLAGS = $(DIST_SANELIBS_LDFLAGS) +libsane_escl_la_LIBADD = $(COMMON_LIBS) libescl.la ../sanei/sanei_init_debug.lo ../sanei/sanei_constrain_value.lo ../sanei/sanei_config.lo sane_strstatus.lo $(JPEG_LIBS) $(XML_LIBS) $(libcurl_LIBS) $(AVAHI_LIBS) +endif +endif +endif +EXTRA_DIST += escl.conf.in + libepjitsu_la_SOURCES = epjitsu.c epjitsu.h epjitsu-cmd.h libepjitsu_la_CPPFLAGS = $(AM_CPPFLAGS) -DBACKEND_NAME=epjitsu diff --git a/backend/escl.conf.in b/backend/escl.conf.in new file mode 100644 index 000000000..2aa625771 --- /dev/null +++ b/backend/escl.conf.in @@ -0,0 +1,17 @@ +# escl.conf -- ESCL configuration +# Lines starting with a # or a ; are comments. Comments must be on a +# line of their own. End-of-line comments are not supported. +# Explanation : if you can't detect your device but it's an eSCL device, modify this escl conf' file to use your device. +# -> uncomment the lines below, from '[device]' to 'port'. +# -> put your device name instead of 'EPSON X'. +# -> put your type of protocol instead of 'https' : http or https. +# -> put your device ip instead of '123.456.789.10'. +# -> put the port that you use instead of '88'. +# For example, the lines below are for one device, but if you have several devices to use, you can duplicate the lines below as many times as you have devices. + +#[device] + +#model EPSON X +#type https +#ip 123.456.789.10 +#port 88 diff --git a/backend/escl/escl.c b/backend/escl/escl.c new file mode 100644 index 000000000..6a82d87eb --- /dev/null +++ b/backend/escl/escl.c @@ -0,0 +1,960 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include +#include + +#include +#include + +#include "../include/sane/saneopts.h" +#include "../include/sane/sanei.h" +#include "../include/sane/sanei_backend.h" +#include "../include/sane/sanei_config.h" +#include "../include/sane/sanei_debug.h" + +#define min(A,B) (((A)<(B)) ? (A) : (B)) +#define max(A,B) (((A)>(B)) ? (A) : (B)) +#define INPUT_BUFFER_SIZE 4096 + +static const SANE_Device **devlist = NULL; +static ESCL_Device *list_devices_primary = NULL; +static int num_devices = 0; + +typedef struct Handled { + struct Handled *next; + SANE_String_Const name; + char *result; + ESCL_ScanParam param; + SANE_Option_Descriptor opt[NUM_OPTIONS]; + Option_Value val[NUM_OPTIONS]; + capabilities_t *scanner; + SANE_Range x_range; + SANE_Range y_range; + unsigned char *img_data; + long img_size; + long img_read; + SANE_Bool cancel; + SANE_Bool write_scan_data; + SANE_Bool decompress_scan_data; + SANE_Bool end_read; + SANE_Parameters ps; +} escl_sane_t; + +struct my_error_mgr +{ + struct jpeg_error_mgr errmgr; + jmp_buf escape; +}; + +typedef struct +{ + struct jpeg_source_mgr pub; + FILE *ctx; + unsigned char buffer[INPUT_BUFFER_SIZE]; +} my_source_mgr; + +/** + * \fn static SANE_Status escl_add_in_list(ESCL_Device *current) + * \brief Function that adds all the element needed to my list : + * the port number, the model name, the ip address, and the type of url (http/https). + * Moreover, this function counts the number of devices found. + * + * \return SANE_STATUS_GOOD if everything is OK. + */ +static SANE_Status +escl_add_in_list(ESCL_Device *current) +{ + ++num_devices; + current->next = list_devices_primary; + list_devices_primary = current; + return (SANE_STATUS_GOOD); +} + +/** + * \fn SANE_Status escl_device_add(int port_nb, const char *model_name, char *ip_address, char *type) + * \brief Function that browses my list ('for' loop) and returns the "escl_add_in_list" function to + * adds all the element needed to my list : + * the port number, the model name, the ip address and the type of the url (http / https). + * + * \return escl_add_in_list(current) + */ +SANE_Status +escl_device_add(int port_nb, const char *model_name, char *ip_address, char *type) +{ + ESCL_Device *current = NULL; + DBG (10, "escl_device_add\n"); + for (current = list_devices_primary; current; current = current->next) { + if (strcmp(current->ip_address, ip_address) == 0 && current->port_nb == port_nb + && strcmp(current->type, type) == 0) + return (SANE_STATUS_GOOD); + } + current = malloc(sizeof(*current)); + if (current == NULL) + return (SANE_STATUS_NO_MEM); + memset(current, 0, sizeof(*current)); + current->port_nb = port_nb; + current->model_name = strdup(model_name); + current->ip_address = strdup(ip_address); + current->type = strdup(type); + return escl_add_in_list(current); +} + +/** + * \fn static inline size_t max_string_size(const SANE_String_Const strings[]) + * \brief Function that browses the string ('for' loop) and counts the number of character in the string. + * --> this allows to know the maximum size of the string. + * + * \return max_size + 1 (the size max) + */ +static inline size_t +max_string_size(const SANE_String_Const strings[]) +{ + size_t max_size = 0; + int i = 0; + + for (i = 0; strings[i]; ++i) { + size_t size = strlen (strings[i]); + if (size > max_size) + max_size = size; + } + return (max_size + 1); +} + +/** + * \fn static SANE_Device *convertFromESCLDev(ESCL_Device *cdev) + * \brief Function that checks if the url of the received scanner is secured or not (http / https). + * --> if the url is not secured, our own url will be composed like "http://'ip':'port'". + * --> else, our own url will be composed like "https://'ip':'port'". + * AND, it's in this function that we gather all the informations of the url (that were in our list) : + * the model_name, the port, the ip, and the type of url. + * SO, leaving this function, we have in memory the complete url. + * + * \return sdev (structure that contains the elements of the url) + */ +static SANE_Device * +convertFromESCLDev(ESCL_Device *cdev) +{ + SANE_Device *sdev = (SANE_Device*) calloc(1, sizeof(SANE_Device)); + char tmp[PATH_MAX] = { 0 }; + + if (strcmp(cdev->type, "_uscan._tcp") == 0 || strcmp(cdev->type, "http") == 0) + snprintf(tmp, sizeof(tmp), "http://%s:%d", cdev->ip_address, cdev->port_nb); + else + snprintf(tmp, sizeof(tmp), "https://%s:%d", cdev->ip_address, cdev->port_nb); + sdev->name = strdup(tmp); + sdev->model = strdup(cdev->model_name); + sdev->vendor = strdup("ESCL"); + sdev->type = strdup("flatbed scanner"); + return (sdev); +} + +/** + * \fn SANE_Status sane_init(SANE_Int *version_code, SANE_Auth_Callback authorize) + * \brief Function that's called before any other SANE function ; it's the first SANE function called. + * --> this function checks the SANE config. and can check the authentication of the user if + * 'authorize' value is more than SANE_TRUE. + * In this case, it will be necessary to define an authentication method. + * + * \return SANE_STATUS_GOOD (everything is OK) + */ +SANE_Status +sane_init(SANE_Int *version_code, SANE_Auth_Callback __sane_unused__ authorize) +{ + DBG_INIT(); + DBG (10, "escl sane_init\n"); + SANE_Status status = SANE_STATUS_GOOD; + + if (version_code != NULL) + *version_code = SANE_VERSION_CODE(1, 0, 0); + if (status != SANE_STATUS_GOOD) + return (status); + return (SANE_STATUS_GOOD); +} + +/** + * \fn void sane_exit(void) + * \brief Function that must be called to terminate use of a backend. + * This function will first close all device handles that still might be open. + * --> by freeing all the elements of my list. + * After this function, no function other than 'sane_init' may be called. + */ +void +sane_exit(void) +{ + DBG (10, "escl sane_exit\n"); + ESCL_Device *next = NULL; + + while (list_devices_primary != NULL) { + next = list_devices_primary->next; + free(list_devices_primary); + list_devices_primary = next; + } + if (devlist) + free (devlist); + list_devices_primary = NULL; + devlist = NULL; +} + +/** + * \fn static SANE_Status attach_one_config(SANEI_Config *config, const char *line) + * \brief Function that implements a configuration file to the user : + * if the user can't detect some devices, he will be able to force their detection with this config' file to use them. + * Thus, this function parses the config' file to use the device of the user with the information below : + * the type of protocol (http/https), the ip, the port number, and the model name. + * + * \return escl_add_in_list(escl_device) if the parsing worked, SANE_STATUS_GOOD otherwise. + */ +static SANE_Status +attach_one_config(SANEI_Config __sane_unused__ *config, const char *line) +{ + int port = 0; + static int count = 0; + static ESCL_Device *escl_device = NULL; + + if (strncmp(line, "[device]", 8) == 0) { + count = 0; + escl_device = (ESCL_Device*)calloc(1, sizeof(ESCL_Device)); + } + if (strncmp(line, "ip", 2) == 0) { + const char *ip_space = sanei_config_skip_whitespace(line + 2); + if (escl_device != NULL && ip_space != NULL) { + count++; + escl_device->ip_address = strdup(ip_space); + } + } + if (sscanf(line, "port %i", &port) == 1 && port != 0) { + const char *port_space = sanei_config_skip_whitespace(line + 4); + if (escl_device != NULL && port_space != NULL) { + count++; + escl_device->port_nb = port; + } + } + if (strncmp(line, "model", 5) == 0) { + const char *model_space = sanei_config_skip_whitespace(line + 5); + if (escl_device != NULL && model_space != NULL) { + count++; + escl_device->model_name = strdup(model_space); + } + } + if (strncmp(line, "type", 4) == 0) { + const char *type_space = sanei_config_skip_whitespace(line + 4); + if (escl_device != NULL && type_space != NULL) { + count++; + escl_device->type = strdup(type_space); + } + } + if (count == 4) + return (escl_add_in_list(escl_device)); + return (SANE_STATUS_GOOD); +} + +/** + * \fn SANE_Status sane_get_devices(const SANE_Device ***device_list, SANE_Bool local_only) + * \brief Function that searches for connected devices and places them in our 'device_list'. ('for' loop) + * If the attribute 'local_only' is worth SANE_FALSE, we only returns the connected devices locally. + * + * \return SANE_STATUS_GOOD if devlist != NULL ; SANE_STATUS_NO_MEM otherwise. + */ +SANE_Status +sane_get_devices(const SANE_Device ***device_list, SANE_Bool local_only) +{ + if (local_only) /* eSCL is a network-only protocol */ + return (device_list ? SANE_STATUS_GOOD : SANE_STATUS_INVAL); + + DBG (10, "escl sane_get_devices\n"); + ESCL_Device *dev = NULL; + static const SANE_Device **devlist = 0; + SANE_Status status; + + if (device_list == NULL) + return (SANE_STATUS_INVAL); + status = sanei_configure_attach(ESCL_CONFIG_FILE, NULL, attach_one_config); + if (status != SANE_STATUS_GOOD) + return (status); + escl_devices(&status); + if (status != SANE_STATUS_GOOD) + return (status); + if (devlist) + free(devlist); + devlist = (const SANE_Device **) calloc (num_devices + 1, sizeof (devlist[0])); + if (devlist == NULL) + return (SANE_STATUS_NO_MEM); + int i = 0; + for (dev = list_devices_primary; i < num_devices; dev = dev->next) { + SANE_Device *s_dev = convertFromESCLDev(dev); + devlist[i] = s_dev; + i++; + } + devlist[i] = 0; + *device_list = devlist; + return (devlist) ? SANE_STATUS_GOOD : SANE_STATUS_NO_MEM; +} + +/** + * \fn static SANE_Status init_options(SANE_String_Const name, escl_sane_t *s) + * \brief Function thzt initializes all the needed options of the received scanner + * (the resolution / the color / the margins) thanks to the informations received with + * the 'escl_capabilities' function, called just before. + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD) + */ +static SANE_Status +init_options(SANE_String_Const name, escl_sane_t *s) +{ + DBG (10, "escl init_options\n"); + SANE_Status status = SANE_STATUS_GOOD; + int i = 0; + + if (name == NULL) + return (SANE_STATUS_INVAL); + 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; + } + s->x_range.min = 0; + s->x_range.max = s->scanner->MaxWidth - s->scanner->MinWidth; + s->x_range.quant = 1; + s->y_range.min = 0; + s->y_range.max = s->scanner->MaxHeight - s->scanner->MinHeight; + s->y_range.quant = 1; + 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; + + 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; + + 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 = s->scanner->ColorModes; + s->val[OPT_MODE].s = (char *)strdup(s->scanner->ColorModes[0]); + s->opt[OPT_MODE].size = max_string_size(s->scanner->ColorModes); + s->scanner->default_color = (char *)strdup(s->scanner->ColorModes[0]); + + s->opt[OPT_RESOLUTION].name = SANE_NAME_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].title = SANE_TITLE_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].desc = SANE_DESC_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].type = SANE_TYPE_INT; + s->opt[OPT_RESOLUTION].unit = SANE_UNIT_DPI; + s->opt[OPT_RESOLUTION].constraint_type = SANE_CONSTRAINT_WORD_LIST; + s->opt[OPT_RESOLUTION].constraint.word_list = s->scanner->SupportedResolutions; + s->val[OPT_RESOLUTION].w = s->scanner->SupportedResolutions[1]; + s->scanner->default_resolution = s->scanner->SupportedResolutions[1]; + + s->opt[OPT_PREVIEW].name = SANE_NAME_PREVIEW; + s->opt[OPT_PREVIEW].title = SANE_TITLE_PREVIEW; + s->opt[OPT_PREVIEW].desc = SANE_DESC_PREVIEW; + s->opt[OPT_PREVIEW].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT; + s->opt[OPT_PREVIEW].type = SANE_TYPE_BOOL; + s->val[OPT_PREVIEW].w = SANE_FALSE; + + s->opt[OPT_GRAY_PREVIEW].name = SANE_NAME_GRAY_PREVIEW; + s->opt[OPT_GRAY_PREVIEW].title = SANE_TITLE_GRAY_PREVIEW; + s->opt[OPT_GRAY_PREVIEW].desc = SANE_DESC_GRAY_PREVIEW; + s->opt[OPT_GRAY_PREVIEW].type = SANE_TYPE_BOOL; + s->val[OPT_GRAY_PREVIEW].w = SANE_FALSE; + + 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; + + 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_FIXED; + s->opt[OPT_TL_X].unit = SANE_UNIT_PIXEL; + s->opt[OPT_TL_X].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_TL_X].constraint.range = &s->x_range; + s->val[OPT_TL_X].w = s->scanner->RiskyLeftMargin; + + 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_FIXED; + s->opt[OPT_TL_Y].unit = SANE_UNIT_PIXEL; + s->opt[OPT_TL_Y].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_TL_Y].constraint.range = &s->y_range; + s->val[OPT_TL_Y].w = s->scanner->RiskyTopMargin; + + 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_FIXED; + s->opt[OPT_BR_X].unit = SANE_UNIT_PIXEL; + s->opt[OPT_BR_X].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_BR_X].constraint.range = &s->x_range; + s->val[OPT_BR_X].w = s->scanner->MaxWidth; + + 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_FIXED; + s->opt[OPT_BR_Y].unit = SANE_UNIT_PIXEL; + s->opt[OPT_BR_Y].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_BR_Y].constraint.range = &s->y_range; + s->val[OPT_BR_Y].w = s->scanner->MaxHeight; + return (status); +} + +/** + * \fn SANE_Status sane_open(SANE_String_Const name, SANE_Handle *h) + * \brief Function that establishes a connection with the device named by 'name', + * and returns a 'handler' using 'SANE_Handle *h', representing it. + * Thus, it's this function that calls the 'escl_status' function firstly, + * then the 'escl_capabilities' function, and, after, the 'init_options' function. + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +sane_open(SANE_String_Const name, SANE_Handle *h) +{ + DBG (10, "escl sane_open\n"); + SANE_Status status; + escl_sane_t *handler = NULL; + + if (name == NULL) + return (SANE_STATUS_INVAL); + status = escl_status(name); + if (status != SANE_STATUS_GOOD) + return (status); + handler = (escl_sane_t *)calloc(1, sizeof(escl_sane_t)); + if (handler == NULL) + return (SANE_STATUS_NO_MEM); + handler->name = strdup(name); + handler->scanner = escl_capabilities(name, &status); + if (status != SANE_STATUS_GOOD) + return (status); + status = init_options(name, handler); + if (status != SANE_STATUS_GOOD) + return (status); + handler->ps.depth = 8; + handler->ps.last_frame = SANE_TRUE; + handler->ps.format = SANE_FRAME_RGB; + handler->ps.pixels_per_line = handler->val[OPT_BR_X].w; + handler->ps.lines = handler->val[OPT_BR_Y].w; + handler->ps.bytes_per_line = handler->ps.pixels_per_line * 3; + status = sane_get_parameters(handler, 0); + if (status != SANE_STATUS_GOOD) + return (status); + handler->cancel = SANE_FALSE; + handler->write_scan_data = SANE_FALSE; + handler->decompress_scan_data = SANE_FALSE; + handler->end_read = SANE_FALSE; + *h = handler; + return (status); +} + +/** + * \fn void sane_cancel(SANE_Handle h) + * \brief Function that's used to, immediately or as quickly as possible, cancel the currently + * pending operation of the device represented by 'SANE_Handle h'. + * This functions calls the 'escl_scanner' functions, that resets the scan operations. + */ +void +sane_cancel(SANE_Handle h) +{ + DBG (10, "escl sane_cancel\n"); + escl_sane_t *handler = h; + + handler->cancel = SANE_TRUE; + escl_scanner(handler->name, handler->result); +} + +/** + * \fn void sane_close(SANE_Handle h) + * \brief Function that closes the communication with the device represented by 'SANE_Handle h'. + * This function must release the resources that were allocated to the opening of 'h'. + */ +void +sane_close(SANE_Handle h) +{ + DBG (10, "escl sane_close\n"); + if (h != NULL) { + free(h); + h = NULL; + } +} + +/** + * \fn const SANE_Option_Descriptor *sane_get_option_descriptor(SANE_Handle h, SANE_Int n) + * \brief Function that retrieves a descriptor from the n number option of the scanner + * represented by 'h'. + * The descriptor remains valid until the machine is closed. + * + * \return s->opt + n + */ +const SANE_Option_Descriptor * +sane_get_option_descriptor(SANE_Handle h, SANE_Int n) +{ + DBG (10, "escl sane_get_option_descriptor\n"); + escl_sane_t *s = h; + + if ((unsigned) n >= NUM_OPTIONS || n < 0) + return (0); + return (s->opt + n); +} + +/** + * \fn SANE_Status sane_control_option(SANE_Handle h, SANE_Int n, SANE_Action a, void *v, SANE_Int *i) + * \brief Function that defines the actions to perform for the 'n' option of the machine, + * represented by 'h', if the action is 'a'. + * There are 3 types of possible actions : + * --> SANE_ACTION_GET_VALUE: 'v' must be used to provide the value of the option. + * --> SANE_ACTION_SET_VALUE: The option must take the 'v' value. + * --> SANE_ACTION_SET_AUTO: The backend or machine must affect the option with an appropriate value. + * Moreover, the parameter 'i' is used to provide additional information about the state of + * 'n' option if SANE_ACTION_SET_VALUE has been performed. + * + * \return SANE_STATUS_GOOD if everything is OK, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL + */ +SANE_Status +sane_control_option(SANE_Handle h, SANE_Int n, SANE_Action a, void *v, SANE_Int *i) +{ + DBG (10, "escl sane_control_option\n"); + escl_sane_t *handler = h; + + if (i) + *i = 0; + if (n >= NUM_OPTIONS || n < 0) + return (SANE_STATUS_INVAL); + if (a == SANE_ACTION_GET_VALUE) { + switch (n) { + case OPT_NUM_OPTS: + case OPT_RESOLUTION: + case OPT_TL_X: + case OPT_TL_Y: + case OPT_BR_X: + case OPT_BR_Y: + case OPT_PREVIEW: + case OPT_GRAY_PREVIEW: + *(SANE_Word *) v = handler->val[n].w; + break; + case OPT_MODE: + strcpy (v, handler->val[n].s); + break; + case OPT_MODE_GROUP: + default: + break; + } + return (SANE_STATUS_GOOD); + } + if (a == SANE_ACTION_SET_VALUE) { + switch (n) { + case OPT_TL_X: + case OPT_TL_Y: + case OPT_BR_X: + case OPT_BR_Y: + case OPT_PREVIEW: + case OPT_GRAY_PREVIEW: + handler->val[n].w = *(SANE_Word *) v; + if (i && handler->val[n].w != *(SANE_Word *) v) + *i |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS | SANE_INFO_INEXACT; + handler->val[n].w = *(SANE_Word *) v; + break; + case OPT_RESOLUTION: + handler->val[n].w = *(SANE_Word *) v; + if (i) + *i |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS | SANE_INFO_INEXACT; + break; + case OPT_MODE: + if (handler->val[n].s) + free (handler->val[n].s); + handler->val[n].s = strdup (v); + if (i) + *i |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS | SANE_INFO_INEXACT; + break; + default: + break; + } + } + return (SANE_STATUS_GOOD); +} + +#if(defined HAVE_LIBJPEG) +static void +error_exit(j_common_ptr cinfo) +{ + longjmp(cinfo->client_data, 1); +} + +/** + * \fn static void get_JPEG_dimension(FILE *fp, int *w, int *h) + * \brief Function that aims to get the dimensions of the jpeg image wich will be scanned. + * This function is called in the "sane_start" function. + */ +static void +get_JPEG_dimension(FILE *fp, int *w, int *h) +{ + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + jmp_buf env; + + cinfo.err = jpeg_std_error(&jerr); + jerr.error_exit = error_exit; + cinfo.client_data = env; + if (setjmp(env)) + return; + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, fp); + jpeg_read_header(&cinfo, TRUE); + cinfo.out_color_space = JCS_RGB; + jpeg_start_decompress(&cinfo); + *w = cinfo.output_width; + *h = cinfo.output_height; + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fseek(fp, SEEK_SET, 0); +} +#endif + +/** + * \fn SANE_Status sane_start(SANE_Handle h) + * \brief Function that initiates aquisition of an image from the device represented by handle 'h'. + * This function calls the "escl_newjob" function and the "escl_scan" function. + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +sane_start(SANE_Handle h) +{ + DBG (10, "escl sane_start\n"); + SANE_Status status = SANE_STATUS_GOOD; + escl_sane_t *handler = h; + int w = 0; + int he = 0; + + if (handler->name == NULL) + return (SANE_STATUS_INVAL); + handler->cancel = SANE_FALSE; + handler->write_scan_data = SANE_FALSE; + handler->decompress_scan_data = SANE_FALSE; + handler->end_read = SANE_FALSE; + handler->scanner->height = handler->val[OPT_BR_Y].w; + handler->scanner->width = handler->val[OPT_BR_X].w; + handler->scanner->pos_x = handler->val[OPT_TL_X].w; + handler->scanner->pos_y = handler->val[OPT_TL_Y].w; + if(handler->scanner->default_color) + free(handler->scanner->default_color); + if (handler->val[OPT_PREVIEW].w == SANE_TRUE) + { + int i = 0, val = 9999;; + if (handler->val[OPT_GRAY_PREVIEW].w == SANE_TRUE || + !strncasecmp(handler->val[OPT_MODE].s, SANE_VALUE_SCAN_MODE_GRAY, 3)) + handler->scanner->default_color = strdup("Grayscale8"); + else + handler->scanner->default_color = strdup("RGB24"); + for (i = 1; i < handler->scanner->SupportedResolutionsSize; i++) + { + if (val > handler->scanner->SupportedResolutions[i]) + val = handler->scanner->SupportedResolutions[i]; + } + handler->scanner->default_resolution = val; + } + else + { + handler->scanner->default_resolution = handler->val[OPT_RESOLUTION].w; + if (!strncasecmp(handler->val[OPT_MODE].s, SANE_VALUE_SCAN_MODE_GRAY, 3)) + handler->scanner->default_color = strdup("Grayscale8"); + else + handler->scanner->default_color = strdup("RGB24"); + } + handler->result = escl_newjob(handler->scanner, handler->name, &status); + if (status != SANE_STATUS_GOOD) + return (status); + status = escl_scan(handler->scanner, handler->name, handler->result); + get_JPEG_dimension(handler->scanner->tmp, &w, &he); + fseek(handler->scanner->tmp, SEEK_SET, 0); + handler->ps.depth = 8; + handler->ps.pixels_per_line = w; + handler->ps.lines = he; + handler->ps.bytes_per_line = w * 3; + handler->ps.last_frame = SANE_TRUE; + handler->ps.format = SANE_FRAME_RGB; + return (status); +} + +/** + * \fn SANE_Status sane_get_parameters(SANE_Handle h, SANE_Parameters *p) + * \brief Function that retrieves the device parameters represented by 'h' and stores them in 'p'. + * This function is normally used after "sane_start". + * It's in this function that we choose to assign the default color. (Color or Monochrome) + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +sane_get_parameters(SANE_Handle h, SANE_Parameters *p) +{ + DBG (10, "escl sane_get_parameters\n"); + SANE_Status status = SANE_STATUS_GOOD; + escl_sane_t *handler = h; + + if (status != SANE_STATUS_GOOD) + return (status); + if (p != NULL) { + p->depth = 8; + p->last_frame = SANE_TRUE; + p->format = SANE_FRAME_RGB; + p->pixels_per_line = handler->ps.pixels_per_line; + p->lines = handler->ps.lines; + p->bytes_per_line = handler->ps.pixels_per_line * 3; + } + return (status); +} + +#if(defined HAVE_LIBJPEG) +/** + * \fn static boolean fill_input_buffer(j_decompress_ptr cinfo) + * \brief Called in the "skip_input_data" function. + * + * \return TRUE (everything is OK) + */ +static boolean +fill_input_buffer(j_decompress_ptr cinfo) +{ + my_source_mgr *src = (my_source_mgr *) cinfo->src; + int nbytes = 0; + + nbytes = fread(src->buffer, 1, INPUT_BUFFER_SIZE, src->ctx); + if (nbytes <= 0) { + src->buffer[0] = (unsigned char) 0xFF; + src->buffer[1] = (unsigned char) JPEG_EOI; + nbytes = 2; + } + src->pub.next_input_byte = src->buffer; + src->pub.bytes_in_buffer = nbytes; + return (TRUE); +} + +/** + * \fn static void skip_input_data(j_decompress_ptr cinfo, long num_bytes) + * \brief Called in the "jpeg_RW_src" function. + */ +static void +skip_input_data(j_decompress_ptr cinfo, long num_bytes) +{ + my_source_mgr *src = (my_source_mgr *) cinfo->src; + + if (num_bytes > 0) { + while (num_bytes > (long) src->pub.bytes_in_buffer) { + num_bytes -= (long) src->pub.bytes_in_buffer; + (void) src->pub.fill_input_buffer(cinfo); + } + src->pub.next_input_byte += (size_t) num_bytes; + src->pub.bytes_in_buffer -= (size_t) num_bytes; + } +} + +static void +term_source(j_decompress_ptr __sane_unused__ cinfo) +{ + return; +} + +static void +init_source(j_decompress_ptr __sane_unused__ cinfo) +{ + return; +} + +/** + * \fn static void jpeg_RW_src(j_decompress_ptr cinfo, FILE *ctx) + * \brief Called in the "escl_sane_decompressor" function. + */ +static void +jpeg_RW_src(j_decompress_ptr cinfo, FILE *ctx) +{ + my_source_mgr *src; + + if (cinfo->src == NULL) { + cinfo->src = (struct jpeg_source_mgr *)(*cinfo->mem->alloc_small) + ((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(my_source_mgr)); + src = (my_source_mgr *) cinfo->src; + } + src = (my_source_mgr *) cinfo->src; + src->pub.init_source = init_source; + src->pub.fill_input_buffer = fill_input_buffer; + src->pub.skip_input_data = skip_input_data; + src->pub.resync_to_restart = jpeg_resync_to_restart; + src->pub.term_source = term_source; + src->ctx = ctx; + src->pub.bytes_in_buffer = 0; + src->pub.next_input_byte = NULL; +} + +static void +my_error_exit(j_common_ptr cinfo) +{ + struct my_error_mgr *err = (struct my_error_mgr *)cinfo->err; + + longjmp(err->escape, 1); +} + +static void +output_no_message(j_common_ptr __sane_unused__ cinfo) +{ +} + +/** + * \fn SANE_Status escl_sane_decompressor(escl_sane_t *handler) + * \brief Function that aims to decompress the jpeg image to SANE be able to read the image. + * This function is called in the "sane_read" function. + * + * \return SANE_STATUS_GOOD (if everything is OK, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +escl_sane_decompressor(escl_sane_t *handler) +{ + int start = 0; + struct jpeg_decompress_struct cinfo; + JSAMPROW rowptr[1]; + unsigned char *surface = NULL; + struct my_error_mgr jerr; + int lineSize = 0; + + if (handler->scanner->tmp == NULL) + return (SANE_STATUS_INVAL); + fseek(handler->scanner->tmp, SEEK_SET, 0); + start = ftell(handler->scanner->tmp); + cinfo.err = jpeg_std_error(&jerr.errmgr); + jerr.errmgr.error_exit = my_error_exit; + jerr.errmgr.output_message = output_no_message; + if (setjmp(jerr.escape)) { + jpeg_destroy_decompress(&cinfo); + if (surface != NULL) + free(surface); + return (SANE_STATUS_INVAL); + } + jpeg_create_decompress(&cinfo); + jpeg_RW_src(&cinfo, handler->scanner->tmp); + jpeg_read_header(&cinfo, TRUE); + cinfo.out_color_space = JCS_RGB; + cinfo.quantize_colors = FALSE; + jpeg_calc_output_dimensions(&cinfo); + surface = malloc(cinfo.output_width * cinfo.output_height * cinfo.output_components); + if (surface == NULL) { + jpeg_destroy_decompress(&cinfo); + fseek(handler->scanner->tmp, start, SEEK_SET); + return (SANE_STATUS_NO_MEM); + } + lineSize = cinfo.output_width * cinfo.output_components; + jpeg_start_decompress(&cinfo); + while (cinfo.output_scanline < cinfo.output_height) { + rowptr[0] = (JSAMPROW)surface + (lineSize * cinfo.output_scanline); + jpeg_read_scanlines(&cinfo, rowptr, (JDIMENSION) 1); + } + handler->img_data = surface; + handler->img_size = lineSize * cinfo.output_height; + handler->img_read = 0; + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(handler->scanner->tmp); + handler->scanner->tmp = NULL; + return (SANE_STATUS_GOOD); +} +#endif + +/** + * \fn SANE_Status sane_read(SANE_Handle h, SANE_Byte *buf, SANE_Int maxlen, SANE_Int *len) + * \brief Function that's used to read image data from the device represented by handle 'h'. + * The 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'. + * --> When the call succeeds, the number of bytes returned can be anywhere in the range from 0 to 'maxlen' bytes. + * + * \return SANE_STATUS_GOOD (if everything is OK, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +sane_read(SANE_Handle h, SANE_Byte *buf, SANE_Int maxlen, SANE_Int *len) +{ + DBG (10, "escl sane_read\n"); + escl_sane_t *handler = h; + SANE_Status status = SANE_STATUS_GOOD; + long readbyte; + + if (!handler | !buf | !len) + return (SANE_STATUS_INVAL); + if (handler->cancel) + return (SANE_STATUS_CANCELLED); + if (!handler->write_scan_data) + handler->write_scan_data = SANE_TRUE; + if (!handler->decompress_scan_data) { + if (handler->scanner->tmp == NULL) + return (SANE_STATUS_INVAL); + status = escl_sane_decompressor(handler); + if (status != SANE_STATUS_GOOD) + return (status); + handler->decompress_scan_data = SANE_TRUE; + } + if (handler->img_data == NULL) + return (SANE_STATUS_INVAL); + if (!handler->end_read) { + readbyte = min((handler->img_size - handler->img_read), maxlen); + memcpy(buf, handler->img_data + handler->img_read, readbyte); + handler->img_read = handler->img_read + readbyte; + *len = readbyte; + if (handler->img_read == handler->img_size) + handler->end_read = SANE_TRUE; + else if (handler->img_read > handler->img_size) { + *len = 0; + handler->end_read = SANE_TRUE; + free(handler->img_data); + handler->img_data = NULL; + return (SANE_STATUS_INVAL); + } + } + else { + *len = 0; + free(handler->img_data); + handler->img_data = NULL; + return (SANE_STATUS_EOF); + } + return (SANE_STATUS_GOOD); +} + +SANE_Status +sane_get_select_fd(SANE_Handle __sane_unused__ h, SANE_Int __sane_unused__ *fd) +{ + return (SANE_STATUS_UNSUPPORTED); +} + +SANE_Status +sane_set_io_mode(SANE_Handle __sane_unused__ handle, SANE_Bool __sane_unused__ non_blocking) +{ + return (SANE_STATUS_UNSUPPORTED); +} diff --git a/backend/escl/escl.h b/backend/escl/escl.h new file mode 100644 index 000000000..559825af2 --- /dev/null +++ b/backend/escl/escl.h @@ -0,0 +1,146 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + + +#ifndef __ESCL_H__ +#define __ESCL_H__ + +#include "../include/sane/config.h" + +#if !(HAVE_LIBCURL && defined(WITH_AVAHI) && defined(HAVE_LIBXML2)) +#error "The escl backend requires libcurl, libavahi and libxml2" +#endif + +#ifndef HAVE_LIBJPEG +/* FIXME: Make JPEG support optional. + Support for PNG and PDF is to be added later but currently only + JPEG is supported. Absence of JPEG support makes the backend a + no-op at present. + */ +#error "The escl backend currently requires libjpeg" +#endif + +#include "../include/sane/sane.h" + +#include + +#ifndef BACKEND_NAME +#define BACKEND_NAME escl +#endif + +#define ESCL_CONFIG_FILE "escl.conf" + +typedef struct { + int p1_0; + int p2_0; + int p3_3; + int DocumentType; + int p4_0; + int p5_0; + int p6_1; + int reserve[11]; +} ESCL_SCANOPTS; + + +typedef struct ESCL_Device { + struct ESCL_Device *next; + + char *model_name; + int port_nb; + char *ip_address; + char *type; +} ESCL_Device; + +typedef struct capabilities +{ + int height; + int width; + int pos_x; + int pos_y; + SANE_String default_color; + SANE_String_Const default_format; + SANE_Int default_resolution; + int MinWidth; + int MaxWidth; + int MinHeight; + int MaxHeight; + int MaxScanRegions; + SANE_String_Const *ColorModes; + int ColorModesSize; + SANE_String_Const *ContentTypes; + int ContentTypesSize; + SANE_String_Const *DocumentFormats; + int DocumentFormatsSize; + SANE_Int *SupportedResolutions; + int SupportedResolutionsSize; + SANE_String_Const *SupportedIntents; + int SupportedIntentsSize; + SANE_String_Const SupportedIntentDefault; + int MaxOpticalXResolution; + int RiskyLeftMargin; + int RiskyRightMargin; + int RiskyTopMargin; + int RiskyBottomMargin; + FILE *tmp; + int format_ext; +} capabilities_t; + +typedef struct { + int XRes; + int YRes; + int Left; + int Top; + int Right; + int Bottom; + int ScanMode; + int ScanMethod; + ESCL_SCANOPTS opts; +} ESCL_ScanParam; + + +enum +{ + OPT_NUM_OPTS = 0, + OPT_MODE_GROUP, + OPT_MODE, + OPT_RESOLUTION, + OPT_PREVIEW, + OPT_GRAY_PREVIEW, + + OPT_GEOMETRY_GROUP, + OPT_TL_X, + OPT_TL_Y, + OPT_BR_X, + OPT_BR_Y, + NUM_OPTIONS +}; + +ESCL_Device *escl_devices(SANE_Status *status); +SANE_Status escl_device_add(int port_nb, const char *model_name, char *ip_address, char *type); +SANE_Status escl_status(SANE_String_Const name); +capabilities_t *escl_capabilities(SANE_String_Const name, SANE_Status *status); +char *escl_newjob(capabilities_t *scanner, SANE_String_Const name, SANE_Status *status); +SANE_Status escl_scan(capabilities_t *scanner, SANE_String_Const name, char *result); +void escl_scanner(SANE_String_Const name, char *result); + +#endif diff --git a/backend/escl/escl_capabilities.c b/backend/escl/escl_capabilities.c new file mode 100644 index 000000000..7e70db8c7 --- /dev/null +++ b/backend/escl/escl_capabilities.c @@ -0,0 +1,337 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + + +#include "escl.h" + +#include +#include +#include + +#include +#include + +#include "../include/sane/saneopts.h" + +struct cap +{ + char *memory; + size_t size; +}; + +/** + * \fn static SANE_String_Const convert_elements(SANE_String_Const str) + * \brief Function that converts the 'color modes' of the scanner (color/gray) to be understood by SANE. + * + * \return SANE_VALUE_SCAN_MODE_GRAY / SANE_VALUE_SCAN_MODE_COLOR ; NULL otherwise + */ +static SANE_String_Const +convert_elements(SANE_String_Const str) +{ + if (strcmp(str, "Grayscale8") == 0) + return (SANE_VALUE_SCAN_MODE_GRAY); + else if (strcmp(str, "RGB24") == 0) + return (SANE_VALUE_SCAN_MODE_COLOR); + return (NULL); +} + +/** + * \fn static SANE_String_Const *char_to_array(SANE_String_Const *tab, int *tabsize, SANE_String_Const mode, int good_array) + * \brief Function that creates the character arrays to put inside : + * the 'color modes', the 'content types', the 'document formats' and the 'supported intents'. + * + * \return board (the allocated array) + */ +static SANE_String_Const * +char_to_array(SANE_String_Const *tab, int *tabsize, SANE_String_Const mode, int good_array) +{ + SANE_String_Const *board = NULL; + int i = 0; + SANE_String_Const convert = NULL; + + if (mode == NULL) + return (tab); + if (good_array != 0) { + convert = convert_elements(mode); + if (convert == NULL) + return (tab); + } + else + convert = mode; + for (i = 0; i < (*tabsize); i++) { + if (strcmp(tab[i], convert) == 0) + return (tab); + } + (*tabsize)++; + if (*tabsize == 1) + board = (SANE_String_Const *)malloc(sizeof(SANE_String_Const) * (*tabsize) + 1); + else + board = (SANE_String_Const *)realloc(tab, sizeof(SANE_String_Const) * (*tabsize) + 1); + board[*tabsize - 1] = (SANE_String_Const)strdup(convert); + board[*tabsize] = NULL; + return (board); +} + +/** + * \fn static SANE_Int *int_to_array(SANE_Int *tab, int *tabsize, int cont) + * \brief Function that creates the integer array to put inside the 'supported resolutions'. + * + * \return board (the allocated array) + */ +static SANE_Int * +int_to_array(SANE_Int *tab, int *tabsize, int cont) +{ + SANE_Int *board = NULL; + int i = 0; + + for (i = 0; i < (*tabsize); i++) { + if (tab[i] == cont) + return (tab); + } + (*tabsize)++; + if (*tabsize == 1) { + (*tabsize)++; + board = malloc(sizeof(SANE_Int *) * (*tabsize) + 1); + } + else + board = realloc(tab, sizeof(SANE_Int *) * (*tabsize) + 1); + board[0] = *tabsize - 1; + board[*tabsize - 1] = cont; + board[*tabsize] = -1; + return (board); +} + +/** + * \fn static size_t memory_callback_c(void *contents, size_t size, size_t nmemb, void *userp) + * \brief Callback function that stocks in memory the content of the scanner capabilities. + * + * \return realsize (size of the content needed -> the scanner capabilities) + */ +static size_t +memory_callback_c(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + struct cap *mem = (struct cap *)userp; + + char *str = realloc(mem->memory, mem->size + realsize + 1); + if (str == NULL) { + fprintf(stderr, "not enough memory (realloc returned NULL)\n"); + return (0); + } + mem->memory = str; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size = mem->size + realsize; + mem->memory[mem->size] = 0; + return (realsize); +} + +/** + * \fn static int find_nodes_c(xmlNode *node) + * \brief Function that browses the xml file and parses it, to find the xml children node. + * --> to recover the scanner capabilities. + * + * \return 0 if a xml child node is found, 1 otherwise + */ +static int +find_nodes_c(xmlNode *node) +{ + xmlNode *child = node->children; + + while (child) { + if (child->type == XML_ELEMENT_NODE) + return (0); + child = child->next; + } + return (1); +} + +/** + * \fn static int find_valor_of_array_variables(xmlNode *node, capabilities_t *scanner) + * \brief Function that searchs in the xml file if a scanner capabilitie stocked + * in one of the created array (character/integer array) is found. + * + * \return 0 + */ +static int +find_valor_of_array_variables(xmlNode *node, capabilities_t *scanner) +{ + const char *name = (const char *)node->name; + if (strcmp(name, "ColorMode") == 0) + scanner->ColorModes = char_to_array(scanner->ColorModes, &scanner->ColorModesSize, (SANE_String_Const)xmlNodeGetContent(node), 1); + else if (strcmp(name, "ContentType") == 0) + scanner->ContentTypes = char_to_array(scanner->ContentTypes, &scanner->ContentTypesSize, (SANE_String_Const)xmlNodeGetContent(node), 0); + else if (strcmp(name, "DocumentFormat") == 0) + scanner->DocumentFormats = char_to_array(scanner->DocumentFormats, &scanner->DocumentFormatsSize, (SANE_String_Const)xmlNodeGetContent(node), 0); + else if (strcmp(name, "DocumentFormatExt") == 0) + scanner->format_ext = 1; + else if (strcmp(name, "Intent") == 0) + scanner->SupportedIntents = char_to_array(scanner->SupportedIntents, &scanner->SupportedIntentsSize, (SANE_String_Const)xmlNodeGetContent(node), 0); + else if (strcmp(name, "XResolution") == 0) + scanner->SupportedResolutions = int_to_array(scanner->SupportedResolutions, &scanner->SupportedResolutionsSize, atoi((const char *)xmlNodeGetContent(node))); + return (0); +} + +/** + * \fn static int find_valor_of_int_variables(xmlNode *node, capabilities_t *scanner) + * \brief Function that searchs in the xml file if a integer scanner capabilitie is found. + * The integer scanner capabilities that are interesting are : + * MinWidth, MaxWidth, MaxHeight, MinHeight, MaxScanRegions, MaxOpticalXResolution, + * RiskyLeftMargin, RiskyRightMargin, RiskyTopMargin, RiskyBottomMargin. + * + * \return 0 + */ +static int +find_valor_of_int_variables(xmlNode *node, capabilities_t *scanner) +{ + int MaxWidth = 0; + int MaxHeight = 0; + const char *name = (const char *)node->name; + + if (strcmp(name, "MinWidth") == 0) + scanner->MinWidth = atoi((const char*)xmlNodeGetContent(node)); + else if (strcmp(name, "MaxWidth") == 0) { + MaxWidth = atoi((const char*)xmlNodeGetContent(node)); + if (scanner->MaxWidth == 0 || MaxWidth < scanner->MaxWidth) + scanner->MaxWidth = atoi((const char *)xmlNodeGetContent(node)); + } + else if (strcmp(name, "MinHeight") == 0) + scanner->MinHeight = atoi((const char*)xmlNodeGetContent(node)); + else if (strcmp(name, "MaxHeight") == 0) { + MaxHeight = atoi((const char*)xmlNodeGetContent(node)); + if (scanner->MaxHeight == 0 || MaxHeight < scanner->MaxHeight) + scanner->MaxHeight = atoi((const char *)xmlNodeGetContent(node)); + } + else if (strcmp(name, "MaxScanRegions") == 0) + scanner->MaxScanRegions = atoi((const char *)xmlNodeGetContent(node)); + else if (strcmp(name, "MaxOpticalXResolution") == 0) + scanner->MaxOpticalXResolution = atoi((const char *)xmlNodeGetContent(node)); + else if (strcmp(name, "RiskyLeftMargin") == 0) + scanner->RiskyLeftMargin = atoi((const char *)xmlNodeGetContent(node)); + else if (strcmp(name, "RiskyRightMargin") == 0) + scanner->RiskyRightMargin = atoi((const char *)xmlNodeGetContent(node)); + else if (strcmp(name, "RiskyTopMargin") == 0) + scanner->RiskyTopMargin = atoi((const char *)xmlNodeGetContent(node)); + else if (strcmp(name, "RiskyBottomMargin") == 0) + scanner->RiskyBottomMargin = atoi((const char *)xmlNodeGetContent(node)); + find_valor_of_array_variables(node, scanner); + return (0); +} + +/** + * \fn static int find_true_variables(xmlNode *node, capabilities_t *scanner) + * \brief Function that searchs in the xml file if we find a scanner capabilitie stocked + * in one of the created array (character/integer array), + * or, if we find a integer scanner capabilitie. + * + * \return 0 + */ +static int +find_true_variables(xmlNode *node, capabilities_t *scanner) +{ + const char *name = (const char *)node->name; + if (strcmp(name, "MinWidth") == 0 || strcmp(name, "MaxWidth") == 0 || strcmp(name, "MinHeight") == 0 || strcmp(name, "MaxHeight") == 0 + || strcmp(name, "MaxScanRegions") == 0 || strcmp(name, "ColorMode") == 0 || strcmp(name, "ContentType") == 0 + || strcmp(name, "DocumentFormat") == 0 || strcmp(name, "XResolution") == 0 || strcmp(name, "Intent") == 0 + || strcmp(name, "MaxOpticalXResolution") == 0 || strcmp(name, "RiskyLeftMargin") == 0 || strcmp(name, "RiskyRightMargin") == 0 + || strcmp(name, "RiskyTopMargin") == 0 || strcmp(name, "RiskyBottomMargin") == 0) + find_valor_of_int_variables(node, scanner); + return (0); +} + +/** + * \fn static int print_xml_c(xmlNode *node, capabilities_t *scanner) + * \brief Function that browses the xml file, node by node. + * + * \return 0 + */ +static int +print_xml_c(xmlNode *node, capabilities_t *scanner) +{ + while (node) { + if (node->type == XML_ELEMENT_NODE) { + if (find_nodes_c(node)) + find_true_variables(node, scanner); + } + print_xml_c(node->children, scanner); + node = node->next; + } + return (0); +} + +/** + * \fn capabilities_t *escl_capabilities(SANE_String_Const name, SANE_Status *status) + * \brief Function that finally recovers all the capabilities of the scanner, using curl. + * This function is called in the 'sane_open' function and it's the equivalent of + * the following curl command : "curl http(s)://'ip':'port'/eSCL/ScannerCapabilities". + * + * \return scanner (the structure that stocks all the capabilities elements) + */ +capabilities_t * +escl_capabilities(SANE_String_Const name, SANE_Status *status) +{ + capabilities_t *scanner = (capabilities_t*)calloc(1, sizeof(capabilities_t)); + CURL *curl_handle = NULL; + struct cap *var = NULL; + xmlDoc *data = NULL; + xmlNode *node = NULL; + const char *scanner_capabilities = "/eSCL/ScannerCapabilities"; + char tmp[PATH_MAX] = { 0 }; + + *status = SANE_STATUS_GOOD; + if (name == NULL) + *status = SANE_STATUS_NO_MEM; + var = (struct cap *)calloc(1, sizeof(struct cap)); + if (var == NULL) + *status = SANE_STATUS_NO_MEM; + var->memory = malloc(1); + var->size = 0; + curl_global_init(CURL_GLOBAL_ALL); + curl_handle = curl_easy_init(); + strcpy(tmp, name); + strcat(tmp, scanner_capabilities); + curl_easy_setopt(curl_handle, CURLOPT_URL, tmp); + if (strncmp(name, "https", 5) == 0) { + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, memory_callback_c); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)var); + if (curl_easy_perform(curl_handle) != CURLE_OK) { + fprintf(stderr, "THERE IS NO SCANNER\n"); + *status = SANE_STATUS_INVAL; + } + data = xmlReadMemory(var->memory, var->size, "file.xml", NULL, 0); + if (data == NULL) + *status = SANE_STATUS_NO_MEM; + node = xmlDocGetRootElement(data); + if (node == NULL) + *status = SANE_STATUS_NO_MEM; + print_xml_c(node, scanner); + xmlFreeDoc(data); + xmlCleanupParser(); + xmlMemoryDump(); + curl_easy_cleanup(curl_handle); + free(var->memory); + curl_global_cleanup(); + return (scanner); +} diff --git a/backend/escl/escl_devices.c b/backend/escl/escl_devices.c new file mode 100644 index 000000000..8adf2fdff --- /dev/null +++ b/backend/escl/escl_devices.c @@ -0,0 +1,147 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "../include/sane/sanei.h" + +static AvahiSimplePoll *simple_poll = NULL; + +/** + * \fn static void resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIndex interface, AVAHI_GCC_UNUSED AvahiProtocol protocol, AvahiResolverEvent event, const char *name, + * const char *type, const char *domain, const char *host_name, const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void *userdata) + * \brief Callback function that will check if the selected scanner follows the escl protocol or not. + */ +static void +resolve_callback(AvahiServiceResolver *r, AVAHI_GCC_UNUSED AvahiIfIndex interface, AVAHI_GCC_UNUSED AvahiProtocol protocol, AvahiResolverEvent event, const char *name, + const char __sane_unused__ *type, const char __sane_unused__ *domain, const char __sane_unused__ *host_name, const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags __sane_unused__ flags, void __sane_unused__ *userdata) +{ + char a[AVAHI_ADDRESS_STR_MAX], *t; + assert(r); + switch (event) { + case AVAHI_RESOLVER_FAILURE: + break; + case AVAHI_RESOLVER_FOUND: + avahi_address_snprint(a, sizeof(a), address); + t = avahi_string_list_to_string(txt); + if (strstr(t, "\"rs=eSCL\"")) + escl_device_add(port, name, a, (char*)type); + } +} + +/** + * \fn static void browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, + * AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, void* userdata) + * \brief Callback function that will browse tanks to 'avahi' the scanners connected in network. + */ +static void +browse_callback(AvahiServiceBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event, const char *name, const char *type, const char *domain, + AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, void* userdata) +{ + AvahiClient *c = userdata; + assert(b); + switch (event) { + case AVAHI_BROWSER_FAILURE: + avahi_simple_poll_quit(simple_poll); + return; + case AVAHI_BROWSER_NEW: + if (!(avahi_service_resolver_new(c, interface, protocol, name, type, domain, AVAHI_PROTO_UNSPEC, 0, resolve_callback, c))) + break; + case AVAHI_BROWSER_REMOVE: + break; + case AVAHI_BROWSER_ALL_FOR_NOW: + case AVAHI_BROWSER_CACHE_EXHAUSTED: + if (event != AVAHI_BROWSER_CACHE_EXHAUSTED) + avahi_simple_poll_quit(simple_poll); + break; + } +} + +/** + * \fn static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *userdata) + * \brief Callback Function that quit if it doesn't find a connected scanner, possible thanks the "Hello Protocol". + * --> Waiting for a answer by the scanner to continue the avahi process. + */ +static void +client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *userdata) +{ + assert(c); + if (state == AVAHI_CLIENT_FAILURE) + avahi_simple_poll_quit(simple_poll); +} + +/** + * \fn ESCL_Device *escl_devices(SANE_Status *status) + * \brief Function that calls all the avahi functions and then, recovers the connected eSCL devices. + * This function is called in the 'sane_get_devices' function. + * + * \return NULL (the eSCL devices found) + */ +ESCL_Device * +escl_devices(SANE_Status *status) +{ + AvahiClient *client = NULL; + AvahiServiceBrowser *sb = NULL; + int error; + + *status = SANE_STATUS_GOOD; + if (!(simple_poll = avahi_simple_poll_new())) { + fprintf(stderr, "Failed to create simple poll object.\n"); + *status = SANE_STATUS_INVAL; + goto fail; + } + client = avahi_client_new(avahi_simple_poll_get(simple_poll), 0, client_callback, NULL, &error); + if (!client) { + fprintf(stderr, "Failed to create client: %s\n", avahi_strerror(error)); + *status = SANE_STATUS_INVAL; + goto fail; + } + if (!(sb = avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_uscan._tcp", NULL, 0, browse_callback, client))) { + fprintf(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_client_errno(client))); + *status = SANE_STATUS_INVAL; + goto fail; + } + if (!(sb = avahi_service_browser_new(client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_uscans._tcp", NULL, 0, browse_callback, client))) { + fprintf(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_client_errno(client))); + *status = SANE_STATUS_INVAL; + goto fail; + } + avahi_simple_poll_loop(simple_poll); +fail: + if (sb) + avahi_service_browser_free(sb); + if (client) + avahi_client_free(client); + if (simple_poll) + avahi_simple_poll_free(simple_poll); + return (NULL); +} diff --git a/backend/escl/escl_newjob.c b/backend/escl/escl_newjob.c new file mode 100644 index 000000000..559bf9785 --- /dev/null +++ b/backend/escl/escl_newjob.c @@ -0,0 +1,207 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include +#include + +#include + +struct uploading +{ + const char *read_data; + size_t size; +}; + +struct downloading +{ + char *memory; + size_t size; +}; + +static const char settings[] = + "" \ + "" \ + " 2.0" \ + " " \ + " " \ + " escl:ThreeHundredthsOfInches" \ + " %d" \ + " %d" \ + " %d" \ + " %d" \ + " " \ + " " \ + " image/jpeg" \ + "%s" \ + " %s" \ + " %d" \ + " %d" \ + " Platen" \ + ""; + +static const char formatExt[] = + " image/jpeg"; + +/** + * \fn static size_t download_callback(void *str, size_t size, size_t nmemb, void *userp) + * \brief Callback function that stocks in memory the content of the 'job'. Example below : + * "Trying 192.168.14.150... + * TCP_NODELAY set + * Connected to 192.168.14.150 (192.168.14.150) port 80 + * POST /eSCL/ScanJobs HTTP/1.1 + * Host: 192.168.14.150 + * User-Agent: curl/7.55.1 + * Accept: / + * Content-Length: 605 + * Content-Type: application/x-www-form-urlencoded + * upload completely sent off: 605 out of 605 bytes + * < HTTP/1.1 201 Created + * < MIME-Version: 1.0 + * < Location: http://192.168.14.150/eSCL/ScanJobs/22b54fd0-027b-1000-9bd0-f4a99726e2fa + * < Content-Length: 0 + * < Connection: close + * < + * Closing connection 0" + * + * \return realsize (size of the content needed -> the 'job') + */ +static size_t +download_callback(void *str, size_t size, size_t nmemb, void *userp) +{ + struct downloading *download = (struct downloading *)userp; + size_t realsize = size * nmemb; + char *content = realloc(download->memory, download->size + realsize + 1); + + if (content == NULL) { + fprintf(stderr, "not enough memory (realloc returned NULL)\n"); + return (0); + } + download->memory = content; + memcpy(&(download->memory[download->size]), str, realsize); + download->size = download->size + realsize; + download->memory[download->size] = 0; + return (realsize); +} + +/** + * \fn char *escl_newjob (capabilities_t *scanner, SANE_String_Const name, SANE_Status *status) + * \brief Function that, using curl, uploads the data (composed by the scanner capabilities) to the + * server to download the 'job' and recover the 'new job' (char *result), in LOCATION. + * This function is called in the 'sane_start' function and it's the equivalent of the + * following curl command : "curl -v POST -d cap.xml http(s)://'ip':'port'/eSCL/ScanJobs". + * + * \return result (the 'new job', situated in LOCATION) + */ +char * +escl_newjob (capabilities_t *scanner, SANE_String_Const name, SANE_Status *status) +{ + CURL *curl_handle = NULL; + struct uploading *upload = NULL; + struct downloading *download = NULL; + const char *scan_jobs = "/eSCL/ScanJobs"; + char cap_data[PATH_MAX] = { 0 }; + char job_cmd[PATH_MAX] = { 0 }; + char *location = NULL; + char *result = NULL; + char *temporary = NULL; + + *status = SANE_STATUS_GOOD; + if (name == NULL || scanner == NULL) { + *status = SANE_STATUS_NO_MEM; + return (NULL); + } + upload = (struct uploading *)calloc(1, sizeof(struct uploading)); + if (upload == NULL) { + *status = SANE_STATUS_NO_MEM; + return (NULL); + } + download = (struct downloading *)calloc(1, sizeof(struct downloading)); + if (download == NULL) { + free(upload); + *status = SANE_STATUS_NO_MEM; + return (NULL); + } + curl_global_init(CURL_GLOBAL_ALL); + curl_handle = curl_easy_init(); + if (curl_handle != NULL) { + snprintf(cap_data, sizeof(cap_data), settings, scanner->height, scanner->width, 0, 0, + (scanner->format_ext == 1 ? formatExt : ""), + scanner->default_color, scanner->default_resolution, scanner->default_resolution); + //fprintf(stderr, "CAP_DATA = %s\n", cap_data); + upload->read_data = strdup(cap_data); + upload->size = strlen(cap_data); + download->memory = malloc(1); + download->size = 0; + strcpy(job_cmd, name); + strcat(job_cmd, scan_jobs); + curl_easy_setopt(curl_handle, CURLOPT_URL, job_cmd); + if (strncmp(name, "https", 5) == 0) { + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + curl_easy_setopt(curl_handle, CURLOPT_POST, 1L); + curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, upload->read_data); + curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, upload->size); + curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, download_callback); + curl_easy_setopt(curl_handle, CURLOPT_HEADERDATA, (void *)download); + if (curl_easy_perform(curl_handle) != CURLE_OK) { + fprintf(stderr, "THERE IS NO SCANNER\n"); + *status = SANE_STATUS_INVAL; + } + else { + if (download->memory != NULL) { + if (strstr(download->memory, "Location:")) { + temporary = strrchr(download->memory, '/'); + if (temporary != NULL) { + location = strchr(temporary, '\r'); + if (location == NULL) + location = strchr(temporary, '\n'); + else { + *location = '\0'; + result = strdup(temporary); + } + } + free(download->memory); + } + else { + fprintf(stderr, "THERE IS NO LOCATION\n"); + *status = SANE_STATUS_INVAL; + } + } + else { + *status = SANE_STATUS_NO_MEM; + return (NULL); + } + } + curl_easy_cleanup(curl_handle); + } + curl_global_cleanup(); + if (upload != NULL) + free(upload); + if (download != NULL) + free(download); + return (result); +} diff --git a/backend/escl/escl_reset.c b/backend/escl/escl_reset.c new file mode 100644 index 000000000..d5bb2f06c --- /dev/null +++ b/backend/escl/escl_reset.c @@ -0,0 +1,72 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include + +#include + +/** + * \fn void escl_scanner(SANE_String_Const name, char *result) + * \brief Function that resets the scanner after each scan, using curl. + * This function is called in the 'sane_cancel' function. + */ +void +escl_scanner(SANE_String_Const name, char *result) +{ + CURL *curl_handle = NULL; + const char *scan_jobs = "/eSCL/ScanJobs"; + const char *scanner_start = "/NextDocument"; + char scan_cmd[PATH_MAX] = { 0 }; + int i = 0; + long answer = 0; + + if (name == NULL || result == NULL) + return; + curl_global_init(CURL_GLOBAL_ALL); +CURL_CALL: + curl_handle = curl_easy_init(); + if (curl_handle != NULL) { + strcpy(scan_cmd, name); + strcat(scan_cmd, scan_jobs); + strcat(scan_cmd, result); + strcat(scan_cmd, scanner_start); + curl_easy_setopt(curl_handle, CURLOPT_URL, scan_cmd); + if (strncmp(name, "https", 5) == 0) { + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + if (curl_easy_perform(curl_handle) == CURLE_OK) { + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &answer); + if (i < 3 && answer == 503) { + curl_easy_cleanup(curl_handle); + i++; + goto CURL_CALL; + } + } + curl_easy_cleanup(curl_handle); + } + curl_global_cleanup(); +} diff --git a/backend/escl/escl_scan.c b/backend/escl/escl_scan.c new file mode 100644 index 000000000..03f523448 --- /dev/null +++ b/backend/escl/escl_scan.c @@ -0,0 +1,96 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include +#include + +#include + +#include "../include/sane/sanei.h" + +/** + * \fn static size_t write_callback(void *str, size_t size, size_t nmemb, void *userp) + * \brief Callback function that writes the image scanned into the temporary file. + * + * \return to_write (the result of the fwrite fonction) + */ +static size_t +write_callback(void *str, size_t size, size_t nmemb, void *userp) +{ + size_t to_write = fwrite(str, size, nmemb, (FILE *)userp); + + return (to_write); +} + +/** + * \fn SANE_Status escl_scan(capabilities_t *scanner, SANE_String_Const name, char *result) + * \brief Function that, after recovering the 'new job', scans the image writed in the + * temporary file, using curl. + * This function is called in the 'sane_start' function and it's the equivalent of + * the following curl command : "curl -s http(s)://'ip:'port'/eSCL/ScanJobs/'new job'/NextDocument > image.jpg". + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +escl_scan(capabilities_t __sane_unused__ *scanner, SANE_String_Const name, char *result) +{ + CURL *curl_handle = NULL; + const char *scan_jobs = "/eSCL/ScanJobs"; + const char *scanner_start = "/NextDocument"; + char scan_cmd[PATH_MAX] = { 0 }; + SANE_Status status = SANE_STATUS_GOOD; + + if (name == NULL) + return (SANE_STATUS_NO_MEM); + curl_global_init(CURL_GLOBAL_ALL); + curl_handle = curl_easy_init(); + if (curl_handle != NULL) { + strcpy(scan_cmd, name); + strcat(scan_cmd, scan_jobs); + strcat(scan_cmd, result); + strcat(scan_cmd, scanner_start); + curl_easy_setopt(curl_handle, CURLOPT_URL, scan_cmd); + if (strncmp(name, "https", 5) == 0) { + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_callback); + scanner->tmp = tmpfile(); + if (scanner->tmp != NULL) { + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, scanner->tmp); + if (curl_easy_perform(curl_handle) != CURLE_OK) { + status = SANE_STATUS_INVAL; + } + else + curl_easy_cleanup(curl_handle); + fseek(scanner->tmp, 0, SEEK_SET); + } + else + status = SANE_STATUS_NO_MEM; + curl_global_cleanup(); + } + return (status); +} diff --git a/backend/escl/escl_status.c b/backend/escl/escl_status.c new file mode 100644 index 000000000..dcdc14bf6 --- /dev/null +++ b/backend/escl/escl_status.c @@ -0,0 +1,173 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2019 Touboul Nathane + Copyright (C) 2019 Thierry HUCHARD + + This file is part of the SANE package. + + SANE 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 3 of the License, or (at your + option) any later version. + + SANE 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 sane; see the file COPYING. If not, write to the Free + Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + This file implements a SANE backend for eSCL scanners. */ + +#include "escl.h" + +#include +#include +#include + +#include +#include + +struct idle +{ + char *memory; + size_t size; +}; + +/** + * \fn static size_t memory_callback_s(void *contents, size_t size, size_t nmemb, void *userp) + * \brief Callback function that stocks in memory the content of the scanner status. + * + * \return realsize (size of the content needed -> the scanner status) + */ +static size_t +memory_callback_s(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + struct idle *mem = (struct idle *)userp; + + char *str = realloc(mem->memory, mem->size + realsize + 1); + if (str == NULL) { + fprintf(stderr, "not enough memory (realloc returned NULL)\n"); + return (0); + } + mem->memory = str; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size = mem->size + realsize; + mem->memory[mem->size] = 0; + return (realsize); +} + +/** + * \fn static int find_nodes_s(xmlNode *node) + * \brief Function that browses the xml file and parses it, to find the xml children node. + * --> to recover the scanner status. + * + * \return 0 if a xml child node is found, 1 otherwise + */ +static int +find_nodes_s(xmlNode *node) +{ + xmlNode *child = node->children; + + while (child) { + if (child->type == XML_ELEMENT_NODE) + return (0); + child = child->next; + } + return (1); +} + +/** + * \fn static void print_xml_s(xmlNode *node, SANE_Status *status) + * \brief Function that browses the xml file, node by node. + * If the node 'State' is found, we are expecting to found in this node the 'Idle' + * content (if the scanner is ready to use) and then 'status' = SANE_STATUS_GOOD. + * Otherwise, this means that the scanner isn't ready to use. + */ +static void +print_xml_s(xmlNode *node, SANE_Status *status) +{ + int x = 0; + + while (node) { + if (node->type == XML_ELEMENT_NODE) { + if (find_nodes_s(node)) { + if (strcmp((const char *)node->name, "State") == 0) + x = 1; + } + if (x == 1 && strcmp((const char *)xmlNodeGetContent(node), "Idle") == 0) + *status = SANE_STATUS_GOOD; + } + print_xml_s(node->children, status); + node = node->next; + } +} + +/** + * \fn SANE_Status escl_status(SANE_String_Const name) + * \brief Function that finally recovers the scanner status ('Idle', or not), using curl. + * This function is called in the 'sane_open' function and it's the equivalent of + * the following curl command : "curl http(s)://'ip':'port'/eSCL/ScannerStatus". + * + * \return status (if everything is OK, status = SANE_STATUS_GOOD, otherwise, SANE_STATUS_NO_MEM/SANE_STATUS_INVAL) + */ +SANE_Status +escl_status(SANE_String_Const name) +{ + SANE_Status status; + CURL *curl_handle = NULL; + struct idle *var = NULL; + xmlDoc *data = NULL; + xmlNode *node = NULL; + const char *scanner_status = "/eSCL/ScannerStatus"; + char tmp[PATH_MAX] = { 0 }; + + if (name == NULL) + return (SANE_STATUS_NO_MEM); + var = (struct idle*)calloc(1, sizeof(struct idle)); + if (var == NULL) + return (SANE_STATUS_NO_MEM); + var->memory = malloc(1); + var->size = 0; + curl_global_init(CURL_GLOBAL_ALL); + curl_handle = curl_easy_init(); + strcpy(tmp, name); + strcat(tmp, scanner_status); + curl_easy_setopt(curl_handle, CURLOPT_URL, tmp); + if (strncmp(name, "https", 5) == 0) { + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + } + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, memory_callback_s); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)var); + if (curl_easy_perform(curl_handle) != CURLE_OK) { + fprintf(stderr, "THERE IS NO SCANNER\n"); + status = SANE_STATUS_INVAL; + goto clean_data; + } + data = xmlReadMemory(var->memory, var->size, "file.xml", NULL, 0); + if (data == NULL) { + status = SANE_STATUS_NO_MEM; + goto clean_data; + } + node = xmlDocGetRootElement(data); + if (node == NULL) { + status = SANE_STATUS_NO_MEM; + goto clean; + } + status = SANE_STATUS_DEVICE_BUSY; + print_xml_s(node, &status); +clean: + xmlFreeDoc(data); +clean_data: + xmlCleanupParser(); + xmlMemoryDump(); + curl_easy_cleanup(curl_handle); + free(var->memory); + free(var); + curl_global_cleanup(); + return (status); +} diff --git a/configure.ac b/configure.ac index 7f7c1a043..bed7e1aff 100644 --- a/configure.ac +++ b/configure.ac @@ -140,6 +140,7 @@ if test "$enable_avahi" = "yes"; then PKG_CHECK_MODULES(AVAHI, [ avahi-client >= 0.6.24 ], [AC_DEFINE(WITH_AVAHI, 1, [define if Avahi support is enabled for saned and the net backend])], enable_avahi=no) fi +AM_CONDITIONAL([have_libavahi], [test x != "x$AVAHI_LIBS"]) dnl check sane to make sure we don't have two installations AC_CHECK_LIB(sane, sane_init, LIBSANE_EXISTS="yes") @@ -430,6 +431,28 @@ AS_IF([test xyes = "x$with_usb" && test xyes != "x$have_usb"], ]) AM_CONDITIONAL([have_usblib], [test x != "x$USB_LIBS"]) +dnl ****************************************************************** +dnl Check for libcurl availability +dnl ****************************************************************** +AC_ARG_WITH(libcurl, + AS_HELP_STRING([--with-libcurl], + [enable functionality that needs libcurl @<:@default=check@:>@]), + [], + [with_libcurl=check]) +AC_DEFINE(HAVE_LIBCURL, + [0], [Define to 1 if libcurl is available]) +AS_IF([test xno != "x$with_libcurl"], + [PKG_CHECK_MODULES(libcurl, [libcurl], + [AC_DEFINE([HAVE_LIBCURL], [1]) + with_libcurl=yes + ], + [AS_IF([test xcheck != "x$with_libcurl"], + [AC_MSG_ERROR([libcurl requested but not found])]) + with_libcurl=no + ]) + ]) +AM_CONDITIONAL([have_libcurl], [test x != "x$libcurl_LIBS"]) + dnl ****************************************************************** dnl Check for USB record/replay support dnl ****************************************************************** @@ -440,6 +463,7 @@ AC_ARG_WITH(usb_record_replay, if test "x$with_usb_record_replay" != "xno"; then PKG_CHECK_MODULES([XML], [libxml-2.0], have_libxml=yes, have_libxml=no) if test "x$have_libxml" = xyes; then + AC_DEFINE(HAVE_LIBXML2, 1, [Define to 1 if libxml2 is available]) AC_DEFINE(WITH_USB_RECORD_REPLAY, 1, [define if USB record replay is enabled]) else if test "x$with_usb_record_replay" = xyes; then @@ -447,6 +471,7 @@ if test "x$with_usb_record_replay" != "xno"; then fi fi fi +AM_CONDITIONAL([have_libxml2], [test x != "x$XML_LIBS"]) dnl ************ dnl SCSI Support @@ -611,8 +636,8 @@ AC_ARG_ENABLE(local-backends, ALL_BACKENDS="abaton agfafocus apple artec artec_eplus48u as6e \ avision bh canon canon630u canon_dr canon_pp cardscan \ coolscan coolscan2 coolscan3 dc25 dc210 dc240 \ - dell1600n_net dmc epjitsu epson epson2 epsonds fujitsu genesys \ - gphoto2 gt68xx hp hp3500 hp3900 hp4200 hp5400 \ + dell1600n_net dmc epjitsu epson epson2 epsonds escl fujitsu \ + genesys gphoto2 gt68xx hp hp3500 hp3900 hp4200 hp5400 \ hp5590 hpsj5s hpljm1005 hs2p ibm kodak kodakaio kvs1025 kvs20xx \ kvs40xx leo lexmark ma1509 magicolor \ matsushita microtek microtek2 mustek mustek_pp \ @@ -841,6 +866,7 @@ else fi echo "IPv6 support: `eval eval echo ${ipv6}`" echo "Avahi support: `eval eval echo ${enable_avahi}`" +echo "cURL support: `eval eval echo ${with_libcurl}`" echo "SNMP support: `eval eval echo ${with_snmp}`" echo "-> The following backends will be built:" for backend in ${BACKENDS} ; do diff --git a/doc/Makefile.am b/doc/Makefile.am index 4b2871d2b..56d35173c 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -19,7 +19,7 @@ EXTRA_DIST = scanimage.man sane-config.man sane-find-scanner.man \ BACKEND_5MANS = sane-abaton.5 sane-agfafocus.5 sane-apple.5 sane-as6e.5 \ sane-dll.5 sane-dc25.5 sane-dmc.5 sane-epson.5 sane-epson2.5 sane-epsonds.5 \ - sane-hp.5 sane-gphoto2.5 sane-leo.5 sane-lexmark.5 \ + sane-escl.5 sane-hp.5 sane-gphoto2.5 sane-leo.5 sane-lexmark.5 \ sane-matsushita.5 sane-microtek.5 sane-microtek2.5 sane-mustek.5 \ sane-nec.5 sane-net.5 sane-pie.5 sane-pieusb.5 sane-pint.5 sane-pnm.5 \ sane-umax.5 sane-qcam.5 sane-scsi.5 sane-artec.5 sane-kodak.5 sane-kodakaio.5 \ @@ -41,8 +41,8 @@ BACKEND_5MANS = sane-abaton.5 sane-agfafocus.5 sane-apple.5 sane-as6e.5 \ EXTRA_DIST += sane-abaton.man sane-agfafocus.man sane-apple.man sane-as6e.man \ sane-dll.man sane-dc25.man sane-dmc.man sane-epson.man \ - sane-epson2.man sane-epsonds.man sane-hp.man sane-gphoto2.man sane-leo.man \ - sane-lexmark.man sane-matsushita.man sane-microtek.man \ + sane-epson2.man sane-epsonds.man sane-escl.man sane-hp.man sane-gphoto2.man \ + sane-leo.man sane-lexmark.man sane-matsushita.man sane-microtek.man \ sane-microtek2.man sane-mustek.man sane-nec.man sane-net.man \ sane-pie.man sane-pieusb.man sane-pint.man sane-pnm.man sane-umax.man \ sane-qcam.man sane-scsi.man sane-artec.man sane-fujitsu.man \ @@ -157,7 +157,7 @@ DESC_FILES = descriptions/abaton.desc descriptions/agfafocus.desc \ descriptions/dc210.desc descriptions/dc240.desc descriptions/dc25.desc \ descriptions/dell1600n_net.desc descriptions/dll.desc descriptions/dmc.desc \ descriptions/epjitsu.desc descriptions/epson2.desc descriptions/epson.desc \ - descriptions/epsonds.desc \ + descriptions/epsonds.desc descriptions/escl.desc \ descriptions/fujitsu.desc descriptions/genesys.desc \ descriptions/gphoto2.desc descriptions/gt68xx.desc descriptions/hp3500.desc \ descriptions/hp3900.desc descriptions/hp4200.desc descriptions/hp5400.desc \ diff --git a/doc/descriptions/escl.desc b/doc/descriptions/escl.desc new file mode 100644 index 000000000..fe9b15395 --- /dev/null +++ b/doc/descriptions/escl.desc @@ -0,0 +1,6 @@ +:backend "escl" +:manpage "sane-escl" +:url "https://support.apple.com/en-us/HT201311" +:comment "The eSCL backend for sane supports AirScan/eSCL devices that announce themselves on mDNS as _uscan._utcp or _uscans._utcp" + +:devicetype :scanner diff --git a/doc/sane-escl.man b/doc/sane-escl.man new file mode 100644 index 000000000..21a4d6c32 --- /dev/null +++ b/doc/sane-escl.man @@ -0,0 +1,41 @@ +.TH sane\-escl 5 "14 Dec 2019" "@PACKAGEVERSION@" "SANE Scanner Access Now Easy" +.IX sane\-escl +.SH NAME +sane\-escl \- SANE backend for eSCL scanners +.SH DESCRIPTION +The +.B sane\-escl +library implements a SANE (Scanner Access Now Easy) backend that +provides access to eSCL protocol scanners. + +.PP +The "escl" backend for SANE supports AirScan/eSCL devices that announce +themselves on mDNS as _uscan._utcp or _uscans._utcp. +If the device is available, the "escl" backend recovers these capacities. +The user configures and starts scanning. +A list of devices that use the eSCL protocol can be found at +.IR https://support.apple.com/en-us/HT201311 . +While these devices are expected to work, your mileage may vary. + +.SH FILES +.TP +.I @CONFIGDIR@/escl.conf +The backend configuration file. +.TP +.I @LIBDIR@/libsane\-escl.a +The static library implementing this backend. +.TP +.I @LIBDIR@/libsane\-escl.so +The shared library implementing this backend (present on systems that +support dynamic loading). +.SH ENVIRONMENT +.TP +.B SANE_DEBUG_ESCL +If the library was compiled with debug support enabled, this +environment variable controls the debug level for this backend. E.g., +a value of 128 requests all debug output to be printed. Smaller +levels reduce verbosity. +.SH "SEE ALSO" +sane(7), scanimage(1), xscanimage(1), xsane(1) +.SH AUTHORS +Touboul Nathane, Thierry HUCHARD