mirror of
https://github.com/openharmony/third_party_sane-airscan.git
synced 2026-06-30 21:17:55 -04:00
25d6ee1e82
These devices has a lot of bugs:
- HTTP statuses 404/410 may indicate temporary unavailability
(normally, HTTP 503 used for this purpose)
- Host name part broken in the Location: header when using IPv6
(Location: http://[fe80/eSCL/ScanJobs/4060, notice unbalanced
square bracket)
- Very long HTTP DELETE timeout after the job completion over
the IPP-USB
Appropriate quirks were added, so now these devices seem to work
correctly when used both via the network and via IPP over USB.
1345 lines
42 KiB
C
1345 lines
42 KiB
C
/* AirScan (a.k.a. eSCL) backend for SANE
|
|
*
|
|
* Copyright (C) 2019 and up by Alexander Pevzner (pzz@apevzner.com)
|
|
* See LICENSE for license terms and conditions
|
|
*
|
|
* ESCL protocol handler
|
|
*/
|
|
|
|
#include "airscan.h"
|
|
|
|
/******************** Protocol constants ********************/
|
|
/* If HTTP 503 reply is received, how many retry attempts
|
|
* to perform before giving up
|
|
*
|
|
* ESCL_RETRY_ATTEMPTS_LOAD - for NextDocument request
|
|
* ESCL_RETRY_ATTEMPTS - for other requests
|
|
*
|
|
* Note, some printers (namely, HP LaserJet MFP M28w) require
|
|
* a lot of retry attempts when loading next page at high res
|
|
*/
|
|
#define ESCL_RETRY_ATTEMPTS_LOAD 30
|
|
#define ESCL_RETRY_ATTEMPTS 10
|
|
|
|
/* And pause between retries, in milliseconds
|
|
*/
|
|
#define ESCL_RETRY_PAUSE 1000
|
|
|
|
/* Some devices (namely, Brother MFC-L2710DW) erroneously returns
|
|
* HTTP 404 Not Found when scanning from ADF, if next LOAD request
|
|
* send immediately after completion the previous one, and ScannerStatus
|
|
* returns ScannerAdfEmpty at this case, which leads to premature
|
|
* scan job termination with SANE_STATUS_NO_DOCS status
|
|
*
|
|
* Introducing a small delay between subsequent LOAD requests solves
|
|
* this problem.
|
|
*
|
|
* To avoid performance regression on a very fast scanners, this
|
|
* delay is limited to some fraction of the preceding LOAD
|
|
* query time
|
|
*
|
|
* ESCL_NEXT_LOAD_DELAY - delay between LOAD requests, milliseconds
|
|
* ESCL_NEXT_LOAD_DELAY_MAX - upper limit of this delay, as a fraction
|
|
* of a previous LOAD time
|
|
*/
|
|
#define ESCL_NEXT_LOAD_DELAY 1000
|
|
#define ESCL_NEXT_LOAD_DELAY_MAX 0.5
|
|
|
|
/* proto_handler_escl represents eSCL protocol handler
|
|
*/
|
|
typedef struct {
|
|
proto_handler proto; /* Base class */
|
|
|
|
/* Miscellaneous flags */
|
|
bool quirk_localhost; /* Set Host: localhost in ScanJobs rq */
|
|
bool quirk_canon_mf410_series; /* Canon MF410 Series */
|
|
bool quirk_port_in_host; /* Always set port in Host: header */
|
|
bool quirk_next_load_delay; /* Use ESCL_NEXT_LOAD_DELAY */
|
|
bool quirk_retry_on_404; /* Retry GET NextDocunemt on HTTP 404 */
|
|
bool quirk_retry_on_410; /* Retry GET NextDocunemt on HTTP 410 */
|
|
bool quirk_broken_ipv6_location; /* Invalid hostname in IPv6 Location: */
|
|
bool quirk_skip_cleanup; /* Don't cleanup after normal operations*/
|
|
} proto_handler_escl;
|
|
|
|
/* XML namespace for XML writer
|
|
*/
|
|
static const xml_ns escl_xml_wr_ns[] = {
|
|
{"pwg", "http://www.pwg.org/schemas/2010/12/sm"},
|
|
{"scan", "http://schemas.hp.com/imaging/escl/2011/05/03"},
|
|
{NULL, NULL}
|
|
};
|
|
|
|
/******************** Miscellaneous types ********************/
|
|
/* escl_scanner_status represents decoded ScannerStatus response
|
|
*/
|
|
typedef struct {
|
|
SANE_Status device_status; /* <pwg:State>XXX</pwg:State> */
|
|
SANE_Status adf_status; /* <scan:AdfState>YYY</scan:AdfState> */
|
|
} escl_scanner_status;
|
|
|
|
|
|
/******************** Forward declarations ********************/
|
|
static error
|
|
escl_parse_scanner_status (const proto_ctx *ctx,
|
|
const char *xml_text, size_t xml_len, escl_scanner_status *out);
|
|
|
|
/******************** HTTP utility functions ********************/
|
|
/* Create HTTP query
|
|
*/
|
|
static http_query*
|
|
escl_http_query (const proto_ctx *ctx, const char *path,
|
|
const char *method, char *body)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
http_query *query = http_query_new_relative(ctx->http, ctx->base_uri, path,
|
|
method, body, "text/xml");
|
|
if (escl->quirk_port_in_host) {
|
|
http_query_force_port(query, true);
|
|
}
|
|
return query;
|
|
}
|
|
|
|
/* Create HTTP get query
|
|
*/
|
|
static http_query*
|
|
escl_http_get (const proto_ctx *ctx, const char *path)
|
|
{
|
|
return escl_http_query(ctx, path, "GET", NULL);
|
|
}
|
|
|
|
/******************** Device Capabilities ********************/
|
|
/* Parse color modes
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_color_modes (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
src->colormodes = 0;
|
|
|
|
xml_rd_enter(xml);
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if(xml_rd_node_name_match(xml, "scan:ColorMode")) {
|
|
const char *v = xml_rd_node_value(xml);
|
|
if (!strcmp(v, "BlackAndWhite1")) {
|
|
src->colormodes |= 1 << ID_COLORMODE_BW1;
|
|
} else if (!strcmp(v, "Grayscale8")) {
|
|
src->colormodes |= 1 << ID_COLORMODE_GRAYSCALE;
|
|
} else if (!strcmp(v, "RGB24")) {
|
|
src->colormodes |= 1 << ID_COLORMODE_COLOR;
|
|
}
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse document formats
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_document_formats (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
xml_rd_enter(xml);
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
unsigned int flags = 0;
|
|
|
|
if(xml_rd_node_name_match(xml, "pwg:DocumentFormat")) {
|
|
flags |= DEVCAPS_SOURCE_PWG_DOCFMT;
|
|
}
|
|
|
|
if(xml_rd_node_name_match(xml, "scan:DocumentFormatExt")) {
|
|
flags |= DEVCAPS_SOURCE_SCAN_DOCFMT_EXT;
|
|
}
|
|
|
|
if (flags != 0) {
|
|
const char *v = xml_rd_node_value(xml);
|
|
ID_FORMAT fmt = id_format_by_mime_name(v);
|
|
|
|
if (fmt != ID_FORMAT_UNKNOWN) {
|
|
src->formats |= 1 << fmt;
|
|
src->flags |= flags;
|
|
}
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse discrete resolutions.
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_discrete_resolutions (xml_rd *xml,
|
|
devcaps_source *src)
|
|
{
|
|
error err = NULL;
|
|
|
|
sane_word_array_reset(&src->resolutions);
|
|
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:DiscreteResolution")) {
|
|
SANE_Word x = 0, y = 0;
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:XResolution")) {
|
|
err = xml_rd_node_value_uint(xml, &x);
|
|
} else if (xml_rd_node_name_match(xml,
|
|
"scan:YResolution")) {
|
|
err = xml_rd_node_value_uint(xml, &y);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
if (x && y && x == y) {
|
|
src->resolutions = sane_word_array_append(src->resolutions, x);
|
|
}
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
if (sane_word_array_len(src->resolutions) > 0) {
|
|
src->flags |= DEVCAPS_SOURCE_RES_DISCRETE;
|
|
sane_word_array_sort(src->resolutions);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Parse resolutions range
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_resolutions_range (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
error err = NULL;
|
|
SANE_Range range_x = {0, 0, 0}, range_y = {0, 0, 0};
|
|
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
SANE_Range *range = NULL;
|
|
if (xml_rd_node_name_match(xml, "scan:XResolution")) {
|
|
range = &range_x;
|
|
} else if (xml_rd_node_name_match(xml, "scan:XResolution")) {
|
|
range = &range_y;
|
|
}
|
|
|
|
if (range != NULL) {
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:Min")) {
|
|
err = xml_rd_node_value_uint(xml, &range->min);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Max")) {
|
|
err = xml_rd_node_value_uint(xml, &range->max);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Step")) {
|
|
err = xml_rd_node_value_uint(xml, &range->quant);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
if (range_x.min > range_x.max) {
|
|
err = ERROR("Invalid scan:XResolution range");
|
|
goto DONE;
|
|
}
|
|
|
|
if (range_y.min > range_y.max) {
|
|
err = ERROR("Invalid scan:YResolution range");
|
|
goto DONE;
|
|
}
|
|
|
|
/* If no quantization value, SANE uses 0, not 1
|
|
*/
|
|
if (range_x.quant == 1) {
|
|
range_x.quant = 0;
|
|
}
|
|
|
|
if (range_y.quant == 1) {
|
|
range_y.quant = 0;
|
|
}
|
|
|
|
/* Try to merge x/y ranges */
|
|
if (!math_range_merge(&src->res_range, &range_x, &range_y)) {
|
|
err = ERROR("Incompatible scan:XResolution and "
|
|
"scan:YResolution ranges");
|
|
goto DONE;
|
|
}
|
|
|
|
src->flags |= DEVCAPS_SOURCE_RES_RANGE;
|
|
|
|
DONE:
|
|
return err;
|
|
}
|
|
|
|
/* Parse supported resolutions.
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_resolutions (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
error err = NULL;
|
|
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:DiscreteResolutions")) {
|
|
err = escl_devcaps_source_parse_discrete_resolutions(xml, src);
|
|
} else if (xml_rd_node_name_match(xml, "scan:ResolutionRange")) {
|
|
err = escl_devcaps_source_parse_resolutions_range(xml, src);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
/* Prefer discrete resolution, if both are provided */
|
|
if (src->flags & DEVCAPS_SOURCE_RES_DISCRETE) {
|
|
src->flags &= ~DEVCAPS_SOURCE_RES_RANGE;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Parse setting profiles (color modes, document formats etc).
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_setting_profiles (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
error err = NULL;
|
|
|
|
/* Parse setting profiles */
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:SettingProfile")) {
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:ColorModes")) {
|
|
err = escl_devcaps_source_parse_color_modes(xml, src);
|
|
} else if (xml_rd_node_name_match(xml,
|
|
"scan:DocumentFormats")) {
|
|
err = escl_devcaps_source_parse_document_formats(xml, src);
|
|
} else if (xml_rd_node_name_match(xml,
|
|
"scan:SupportedResolutions")) {
|
|
err = escl_devcaps_source_parse_resolutions(xml, src);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
/* Validate results */
|
|
if (err == NULL) {
|
|
src->colormodes &= DEVCAPS_COLORMODES_SUPPORTED;
|
|
if (src->colormodes == 0) {
|
|
return ERROR("no color modes detected");
|
|
}
|
|
|
|
src->formats &= DEVCAPS_FORMATS_SUPPORTED;
|
|
if (src->formats == 0) {
|
|
return ERROR("no image formats detected");
|
|
}
|
|
|
|
if (!(src->flags & (DEVCAPS_SOURCE_RES_DISCRETE|
|
|
DEVCAPS_SOURCE_RES_RANGE))){
|
|
return ERROR("scan resolutions are not defined");
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Parse supported intents (photo/document etc).
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse_supported_intents (xml_rd *xml, devcaps_source *src)
|
|
{
|
|
error err = NULL;
|
|
|
|
xml_rd_enter(xml);
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if(xml_rd_node_name_match(xml, "scan:SupportedIntent") ||
|
|
xml_rd_node_name_match(xml, "scan:Intent")) {
|
|
const char *v = xml_rd_node_value(xml);
|
|
if (!strcmp(v, "Document")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_DOCUMENT;
|
|
} else if (!strcmp(v, "TextAndGraphic")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_TEXTANDGRAPHIC;
|
|
} else if (!strcmp(v, "Photo")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_PHOTO;
|
|
} else if (!strcmp(v, "Preview")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_PREVIEW;
|
|
} else if (!strcmp(v, "Object")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_OBJECT;
|
|
} else if (!strcmp(v, "BusinessCard")) {
|
|
src->scanintents |= 1 << ID_SCANINTENT_BUSINESSCARD;
|
|
} else {
|
|
log_debug(NULL, "unknown intent: %s", v);
|
|
}
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
/* Parse ADF justification
|
|
*/
|
|
static void
|
|
escl_devcaps_parse_justification (xml_rd *xml,
|
|
ID_JUSTIFICATION *x, ID_JUSTIFICATION *y)
|
|
{
|
|
xml_rd_enter(xml);
|
|
|
|
*x = *y = ID_JUSTIFICATION_UNKNOWN;
|
|
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if(xml_rd_node_name_match(xml, "pwg:XImagePosition")){
|
|
const char *v = xml_rd_node_value(xml);
|
|
if (!strcmp(v, "Right")){
|
|
*x = ID_JUSTIFICATION_RIGHT;
|
|
} else if (!strcmp(v, "Center")) {
|
|
*x = ID_JUSTIFICATION_CENTER;
|
|
} else if (!strcmp(v, "Left")) {
|
|
*x = ID_JUSTIFICATION_LEFT;
|
|
}
|
|
} else if(xml_rd_node_name_match(xml, "pwg:YImagePosition")){
|
|
const char *v = xml_rd_node_value(xml);
|
|
if (!strcmp(v, "Top")){
|
|
*y = ID_JUSTIFICATION_TOP;
|
|
} else if (!strcmp(v, "Center")) {
|
|
*y = ID_JUSTIFICATION_CENTER;
|
|
} else if (!strcmp(v, "Bottom")) {
|
|
*y = ID_JUSTIFICATION_BOTTOM;
|
|
}
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
}
|
|
|
|
/* Parse source capabilities. Returns NULL on success, error string otherwise
|
|
*/
|
|
static error
|
|
escl_devcaps_source_parse (xml_rd *xml, devcaps_source **out)
|
|
{
|
|
devcaps_source *src = devcaps_source_new();
|
|
error err = NULL;
|
|
|
|
xml_rd_enter(xml);
|
|
for (; err == NULL && !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if(xml_rd_node_name_match(xml, "scan:MinWidth")) {
|
|
err = xml_rd_node_value_uint(xml, &src->min_wid_px);
|
|
} else if (xml_rd_node_name_match(xml, "scan:MaxWidth")) {
|
|
err = xml_rd_node_value_uint(xml, &src->max_wid_px);
|
|
} else if (xml_rd_node_name_match(xml, "scan:MinHeight")) {
|
|
err = xml_rd_node_value_uint(xml, &src->min_hei_px);
|
|
} else if (xml_rd_node_name_match(xml, "scan:MaxHeight")) {
|
|
err = xml_rd_node_value_uint(xml, &src->max_hei_px);
|
|
} else if (xml_rd_node_name_match(xml, "scan:SettingProfiles")) {
|
|
err = escl_devcaps_source_parse_setting_profiles(xml, src);
|
|
} else if (xml_rd_node_name_match(xml, "scan:SupportedIntents")) {
|
|
err = escl_devcaps_source_parse_supported_intents(xml, src);
|
|
}
|
|
}
|
|
xml_rd_leave(xml);
|
|
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
|
|
if (src->max_wid_px != 0 && src->max_hei_px != 0)
|
|
{
|
|
/* Validate window size */
|
|
if (src->min_wid_px > src->max_wid_px)
|
|
{
|
|
err = ERROR("Invalid scan:MinWidth or scan:MaxWidth");
|
|
goto DONE;
|
|
}
|
|
|
|
if (src->min_hei_px > src->max_hei_px)
|
|
{
|
|
err = ERROR("Invalid scan:MinHeight or scan:MaxHeight");
|
|
goto DONE;
|
|
}
|
|
|
|
src->flags |= DEVCAPS_SOURCE_HAS_SIZE;
|
|
|
|
/* Set window ranges */
|
|
src->win_x_range_mm.min = src->win_y_range_mm.min = 0;
|
|
src->win_x_range_mm.max = math_px2mm_res(src->max_wid_px, 300);
|
|
src->win_y_range_mm.max = math_px2mm_res(src->max_hei_px, 300);
|
|
}
|
|
|
|
DONE:
|
|
if (err != NULL) {
|
|
devcaps_source_free(src);
|
|
} else {
|
|
if (*out == NULL) {
|
|
*out = src;
|
|
} else {
|
|
/* Duplicate detected. Ignored for now */
|
|
devcaps_source_free(src);
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Parse compression factor parameters
|
|
*/
|
|
static error
|
|
escl_devcaps_compression_parse (xml_rd *xml, devcaps *caps)
|
|
{
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
error err = NULL;
|
|
|
|
if (xml_rd_node_name_match(xml, "scan:Min")) {
|
|
err = xml_rd_node_value_uint(xml, &caps->compression_range.min);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Max")) {
|
|
err = xml_rd_node_value_uint(xml, &caps->compression_range.max);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Step")) {
|
|
err = xml_rd_node_value_uint(xml, &caps->compression_range.quant);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Normal")) {
|
|
err = xml_rd_node_value_uint(xml, &caps->compression_norm);
|
|
}
|
|
|
|
if (err != NULL) {
|
|
return err;
|
|
}
|
|
}
|
|
|
|
/* Validate obtained parameters.
|
|
*
|
|
* Note, errors are silently ignored starting from this point
|
|
*/
|
|
if (caps->compression_range.min > caps->compression_range.max) {
|
|
return NULL;
|
|
}
|
|
|
|
if (caps->compression_norm < caps->compression_range.min ||
|
|
caps->compression_norm > caps->compression_range.max) {
|
|
return NULL;
|
|
}
|
|
|
|
caps->compression_ok = true;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Parse device capabilities. devcaps structure must be initialized
|
|
* before calling this function.
|
|
*/
|
|
static error
|
|
escl_devcaps_parse (proto_handler_escl *escl,
|
|
devcaps *caps, const char *xml_text, size_t xml_len)
|
|
{
|
|
error err = NULL;
|
|
xml_rd *xml;
|
|
bool quirk_canon_iR2625_2630 = false;
|
|
ID_SOURCE id_src;
|
|
bool src_ok = false;
|
|
|
|
/* Fill "constant" part of device capabilities */
|
|
caps->units = 300;
|
|
caps->protocol = escl->proto.name;
|
|
caps->justification_x = caps->justification_y = ID_JUSTIFICATION_UNKNOWN;
|
|
|
|
/* Parse capabilities XML */
|
|
err = xml_rd_begin(&xml, xml_text, xml_len, NULL);
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
|
|
if (!xml_rd_node_name_match(xml, "scan:ScannerCapabilities")) {
|
|
err = ERROR("XML: missed scan:ScannerCapabilities");
|
|
goto DONE;
|
|
}
|
|
|
|
xml_rd_enter(xml);
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "pwg:MakeAndModel")) {
|
|
const char *m = xml_rd_node_value(xml);
|
|
|
|
if (!strcmp(m, "Canon iR2625/2630")) {
|
|
quirk_canon_iR2625_2630 = true;
|
|
} else if (!strcmp(m, "HP LaserJet MFP M630")) {
|
|
escl->quirk_localhost = true;
|
|
} else if (!strcmp(m, "HP Color LaserJet FlowMFP M578")) {
|
|
escl->quirk_localhost = true;
|
|
} else if (!strcmp(m, "MF410 Series")) {
|
|
escl->quirk_canon_mf410_series = true;
|
|
} else if (!strncasecmp(m, "EPSON ", 6)) {
|
|
escl->quirk_port_in_host = true;
|
|
} else if (!strncasecmp(m, "Brother ", 8)) {
|
|
escl->quirk_next_load_delay = true;
|
|
} else if (!strcmp(m, "B205") || !strcmp(m, "B215")) {
|
|
/* Xerox B205/B215 machines may indicate temporary
|
|
* unavailability of the scanned document (the need
|
|
* for retry) using HTTP 404/410 codes. Normally,
|
|
* HTTP 503 used for this purpose...
|
|
*/
|
|
escl->quirk_retry_on_404 = true;
|
|
escl->quirk_retry_on_410 = true;
|
|
escl->quirk_broken_ipv6_location = true;
|
|
escl->quirk_skip_cleanup = true;
|
|
}
|
|
} else if (xml_rd_node_name_match(xml, "scan:Manufacturer")) {
|
|
const char *m = xml_rd_node_value(xml);
|
|
|
|
if (!strcasecmp(m, "EPSON")) {
|
|
escl->quirk_port_in_host = true;
|
|
}
|
|
} else if (xml_rd_node_name_match(xml, "scan:Platen")) {
|
|
xml_rd_enter(xml);
|
|
if (xml_rd_node_name_match(xml, "scan:PlatenInputCaps")) {
|
|
err = escl_devcaps_source_parse(xml,
|
|
&caps->src[ID_SOURCE_PLATEN]);
|
|
}
|
|
xml_rd_leave(xml);
|
|
} else if (xml_rd_node_name_match(xml, "scan:Adf")) {
|
|
xml_rd_enter(xml);
|
|
while (!xml_rd_end(xml)) {
|
|
if (xml_rd_node_name_match(xml, "scan:AdfSimplexInputCaps")) {
|
|
err = escl_devcaps_source_parse(xml,
|
|
&caps->src[ID_SOURCE_ADF_SIMPLEX]);
|
|
} else if (xml_rd_node_name_match(xml,
|
|
"scan:AdfDuplexInputCaps")) {
|
|
err = escl_devcaps_source_parse(xml,
|
|
&caps->src[ID_SOURCE_ADF_DUPLEX]);
|
|
}
|
|
else if (xml_rd_node_name_match(xml, "scan:Justification")) {
|
|
escl_devcaps_parse_justification(xml,
|
|
&caps->justification_x, &caps->justification_y);
|
|
}
|
|
xml_rd_next(xml);
|
|
}
|
|
xml_rd_leave(xml);
|
|
} else if (xml_rd_node_name_match(xml, "scan:CompressionFactorSupport")) {
|
|
xml_rd_enter(xml);
|
|
err = escl_devcaps_compression_parse(xml, caps);
|
|
xml_rd_leave(xml);
|
|
}
|
|
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
}
|
|
|
|
/* Check that we have at least one source */
|
|
for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) {
|
|
if (caps->src[id_src] != NULL) {
|
|
src_ok = true;
|
|
}
|
|
}
|
|
|
|
if (!src_ok) {
|
|
err = ERROR("XML: neither platen nor ADF sources detected");
|
|
goto DONE;
|
|
}
|
|
|
|
/* Apply quirks, if any */
|
|
if (quirk_canon_iR2625_2630) {
|
|
/* This device announces resolutions up to 600 DPI,
|
|
* but actually doesn't support more that 300
|
|
*
|
|
* https://oip.manual.canon/USRMA-4209-zz-CSL-2600-enUV/contents/devu-apdx-sys_spec-send.html?search=600
|
|
*
|
|
* See #57 for details
|
|
*/
|
|
for (id_src = (ID_SOURCE) 0; id_src < NUM_ID_SOURCE; id_src ++) {
|
|
devcaps_source *src = caps->src[id_src];
|
|
if (src != NULL &&
|
|
/* paranoia: array won't be empty after quirk applied */
|
|
sane_word_array_len(src->resolutions) > 0 &&
|
|
src->resolutions[1] <= 300) {
|
|
sane_word_array_bound(src->resolutions, 0, 300);
|
|
}
|
|
}
|
|
}
|
|
|
|
DONE:
|
|
if (err != NULL) {
|
|
devcaps_reset(caps);
|
|
}
|
|
|
|
xml_rd_finish(&xml);
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Query device capabilities
|
|
*/
|
|
static http_query*
|
|
escl_devcaps_query (const proto_ctx *ctx)
|
|
{
|
|
return escl_http_get(ctx, "ScannerCapabilities");
|
|
}
|
|
|
|
/* Decode device capabilities
|
|
*/
|
|
static error
|
|
escl_devcaps_decode (const proto_ctx *ctx, devcaps *caps)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
http_data *data = http_query_get_response_data(ctx->query);
|
|
const char *s;
|
|
|
|
/* Most of devices that have Server: HP_Compact_Server
|
|
* in their HTTP response header, require this quirk
|
|
* (see #116)
|
|
*/
|
|
s = http_query_get_response_header(ctx->query, "server");
|
|
if (s != NULL && !strcmp(s, "HP_Compact_Server")) {
|
|
escl->quirk_localhost = true;
|
|
}
|
|
|
|
return escl_devcaps_parse(escl, caps, data->bytes, data->size);
|
|
}
|
|
|
|
/* Create pre-scan check query
|
|
*/
|
|
static http_query*
|
|
escl_precheck_query (const proto_ctx *ctx)
|
|
{
|
|
return escl_http_get(ctx, "ScannerStatus");
|
|
}
|
|
|
|
/* Decode pre-scan check query results
|
|
*/
|
|
static proto_result
|
|
escl_precheck_decode (const proto_ctx *ctx)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
proto_result result = {0};
|
|
error err = NULL;
|
|
escl_scanner_status sts;
|
|
bool adf = ctx->params.src == ID_SOURCE_ADF_SIMPLEX ||
|
|
ctx->params.src == ID_SOURCE_ADF_DUPLEX;
|
|
|
|
/* Initialize result to something optimistic :-) */
|
|
result.status = SANE_STATUS_GOOD;
|
|
result.next = PROTO_OP_SCAN;
|
|
|
|
/* Decode status */
|
|
err = http_query_error(ctx->query);
|
|
if (err == NULL) {
|
|
http_data *data = http_query_get_response_data(ctx->query);
|
|
err = escl_parse_scanner_status(ctx, data->bytes, data->size, &sts);
|
|
}
|
|
|
|
if (err != NULL) {
|
|
result.err = err;
|
|
result.status = SANE_STATUS_IO_ERROR;
|
|
result.next = PROTO_OP_FINISH;
|
|
return result;
|
|
}
|
|
|
|
/* Note, the pre-check status is not always reliable, so normally
|
|
* we ignore it. However, with Canon MF410 Series attempt to
|
|
* scan from empty ADF causes ADF jam error (really, physical!),
|
|
* so we must take care
|
|
*/
|
|
if (escl->quirk_canon_mf410_series) {
|
|
if (adf) {
|
|
switch (sts.adf_status) {
|
|
case SANE_STATUS_JAMMED:
|
|
case SANE_STATUS_NO_DOCS:
|
|
result.status = sts.adf_status;
|
|
result.next = PROTO_OP_FINISH;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Fix Location: URL
|
|
*
|
|
* It replaces host part of uri with the host part from `orig_uri` and
|
|
* used for two purposes:
|
|
* 1) To fix URL in the Location: header. We don't trust the device
|
|
* and only allow it to specify path, not host (host is preserved
|
|
* from the `orig_uri` -- the URL used to initiate scanning
|
|
* 2) As redirection callback, together with the quirk_localhost.
|
|
* At this case, Host: is purposely invalid, which makes
|
|
* redirection unreliable (most likely, redirection will
|
|
* use "localhost" from the Host: header as the host part
|
|
* of URL).
|
|
*
|
|
* Can be directly used as http_query_onredir() callback
|
|
*/
|
|
static void
|
|
escl_scan_fix_location (void *p, http_uri *uri, const http_uri *orig_uri)
|
|
{
|
|
(void) p;
|
|
http_uri_fix_host(uri, orig_uri, "localhost");
|
|
}
|
|
|
|
/* Initiate scanning
|
|
*/
|
|
static http_query*
|
|
escl_scan_query (const proto_ctx *ctx)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
const proto_scan_params *params = &ctx->params;
|
|
const char *source = NULL;
|
|
const char *colormode = NULL;
|
|
const char *scanintent = NULL;
|
|
const char *mime = id_format_mime_name(ctx->params.format);
|
|
const devcaps_source *src = ctx->devcaps->src[params->src];
|
|
bool duplex = false;
|
|
http_query *query;
|
|
|
|
/* Prepare parameters */
|
|
switch (params->src) {
|
|
case ID_SOURCE_PLATEN: source = "Platen"; duplex = false; break;
|
|
case ID_SOURCE_ADF_SIMPLEX: source = "Feeder"; duplex = false; break;
|
|
case ID_SOURCE_ADF_DUPLEX: source = "Feeder"; duplex = true; break;
|
|
|
|
default:
|
|
log_internal_error(ctx->log);
|
|
}
|
|
|
|
switch (params->colormode) {
|
|
case ID_COLORMODE_COLOR: colormode = "RGB24"; break;
|
|
case ID_COLORMODE_GRAYSCALE: colormode = "Grayscale8"; break;
|
|
case ID_COLORMODE_BW1: colormode = "BlackAndWhite1"; break;
|
|
|
|
default:
|
|
log_internal_error(ctx->log);
|
|
}
|
|
|
|
switch (params->scanintent) {
|
|
case ID_SCANINTENT_UNSET: break;
|
|
case ID_SCANINTENT_DOCUMENT: scanintent = "Document"; break;
|
|
case ID_SCANINTENT_TEXTANDGRAPHIC: scanintent = "TextAndGraphic"; break;
|
|
case ID_SCANINTENT_PHOTO: scanintent = "Photo"; break;
|
|
case ID_SCANINTENT_PREVIEW: scanintent = "Preview"; break;
|
|
case ID_SCANINTENT_OBJECT: scanintent = "Object"; break;
|
|
case ID_SCANINTENT_BUSINESSCARD: scanintent = "BusinessCard"; break;
|
|
|
|
default:
|
|
log_internal_error(ctx->log);
|
|
}
|
|
|
|
/* Build scan request */
|
|
xml_wr *xml = xml_wr_begin("scan:ScanSettings", escl_xml_wr_ns);
|
|
|
|
xml_wr_add_text(xml, "pwg:Version", "2.0");
|
|
|
|
if (scanintent) {
|
|
xml_wr_add_text(xml, "scan:Intent", scanintent);
|
|
}
|
|
|
|
xml_wr_enter(xml, "pwg:ScanRegions");
|
|
xml_wr_enter(xml, "pwg:ScanRegion");
|
|
xml_wr_add_text(xml, "pwg:ContentRegionUnits",
|
|
"escl:ThreeHundredthsOfInches");
|
|
xml_wr_add_uint(xml, "pwg:XOffset", params->x_off);
|
|
xml_wr_add_uint(xml, "pwg:YOffset", params->y_off);
|
|
xml_wr_add_uint(xml, "pwg:Width", params->wid);
|
|
xml_wr_add_uint(xml, "pwg:Height", params->hei);
|
|
xml_wr_leave(xml); /* pwg:ScanRegion */
|
|
xml_wr_leave(xml); /* pwg:ScanRegions */
|
|
|
|
//xml_wr_add_text(xml, "scan:InputSource", source);
|
|
xml_wr_add_text(xml, "pwg:InputSource", source);
|
|
if (ctx->devcaps->compression_ok) {
|
|
xml_wr_add_uint(xml, "scan:CompressionFactor",
|
|
ctx->devcaps->compression_norm);
|
|
}
|
|
xml_wr_add_text(xml, "scan:ColorMode", colormode);
|
|
xml_wr_add_text(xml, "pwg:DocumentFormat", mime);
|
|
if ((src->flags & DEVCAPS_SOURCE_SCAN_DOCFMT_EXT) != 0) {
|
|
xml_wr_add_text(xml, "scan:DocumentFormatExt", mime);
|
|
}
|
|
xml_wr_add_uint(xml, "scan:XResolution", params->x_res);
|
|
xml_wr_add_uint(xml, "scan:YResolution", params->y_res);
|
|
if (params->src != ID_SOURCE_PLATEN) {
|
|
xml_wr_add_bool(xml, "scan:Duplex", duplex);
|
|
}
|
|
|
|
/* Send request to device */
|
|
query = escl_http_query(ctx, "ScanJobs", "POST",
|
|
xml_wr_finish_compact(xml));
|
|
|
|
/* Kyocera ECOSYS M6526cdn drops TLS connection after sending
|
|
* response HTTP headers, but before the body transfer is completed.
|
|
*
|
|
* As for this request we are only interested in the response
|
|
* headers, we can ignore this kind of error
|
|
*
|
|
* See here for details:
|
|
* https://github.com/alexpevzner/sane-airscan/issues/163
|
|
*/
|
|
http_query_no_need_response_body(query);
|
|
|
|
/* It's a dirty hack
|
|
*
|
|
* HP LaserJet MFP M630, HP Color LaserJet FlowMFP M578 and
|
|
* probably some other HP devices don't allow eSCL scan, unless
|
|
* Host is set to "localhost". It is probably bad and naive attempt
|
|
* to enforce some access security.
|
|
*
|
|
* So here we forcibly set Host to "localhost".
|
|
*
|
|
* Note, this hack doesn't work with some other printers
|
|
* see #92, #98 for details
|
|
*/
|
|
if (escl->quirk_localhost && !http_uri_is_loopback(ctx->base_uri)) {
|
|
http_query_set_request_header(query, "Host", "localhost");
|
|
http_query_onredir(query, escl_scan_fix_location);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/* Decode result of scan request
|
|
*/
|
|
static proto_result
|
|
escl_scan_decode (const proto_ctx *ctx)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
proto_result result = {0};
|
|
error err = NULL;
|
|
const char *location;
|
|
http_uri *uri;
|
|
|
|
/* Check HTTP status */
|
|
if (http_query_status(ctx->query) != HTTP_STATUS_CREATED) {
|
|
err = eloop_eprintf("ScanJobs request: unexpected HTTP status %d",
|
|
http_query_status(ctx->query));
|
|
result.next = PROTO_OP_CHECK;
|
|
result.err = err;
|
|
return result;
|
|
}
|
|
|
|
/* Obtain location */
|
|
location = http_query_get_response_header(ctx->query, "Location");
|
|
if (location == NULL || *location == '\0') {
|
|
err = eloop_eprintf("ScanJobs request: empty location received");
|
|
goto ERROR;
|
|
}
|
|
|
|
/* Fix broken IPv6 location */
|
|
if (escl->quirk_broken_ipv6_location) {
|
|
/* Xerox B205/B215 return Location that looks like this:
|
|
*
|
|
* Location: http://[fe80/eSCL/ScanJobs/4060
|
|
*
|
|
* Note unbalanced square bracket and truncated IPv6 address.
|
|
* As we anyway ignore Location: hostname, it is easy to fix...
|
|
*/
|
|
if (str_has_prefix(location, "http://[") ||
|
|
str_has_prefix(location, "https://[")) {
|
|
if (strchr(location, ']') == NULL) {
|
|
const char *s = strstr(location, "://");
|
|
s += 3;
|
|
while (*s && *s != '/') {
|
|
s ++;
|
|
}
|
|
if (*s == '/') {
|
|
/* Skip URL until /path/...
|
|
*/
|
|
log_debug(ctx->log, "Broken IPv6 Location: %s", location);
|
|
location = s;
|
|
log_debug(ctx->log, "Fixed Location: %s", location);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Validate and save location */
|
|
uri = http_uri_new_relative(ctx->base_uri, location, true, false);
|
|
if (uri == NULL) {
|
|
err = eloop_eprintf("ScanJobs request: invalid location received");
|
|
goto ERROR;
|
|
}
|
|
|
|
/* Don't trust hostname in Location, replace it with hostname
|
|
* from the ctx->query (which represents scan request)
|
|
*
|
|
* The initial idea behind this approach is that scanner may
|
|
* not have a strong knowledge of its own host name so hostname
|
|
* supplied by device may be inaccurate.
|
|
*
|
|
* At least one device, HP Deskjet 3520 series, has demonstrated
|
|
* this behavior when scanning via IPP over USB. Instead of (expected)
|
|
* localhost host name, it returns CN26F178X605SZ.03f011b0.hpLedmUSB
|
|
*/
|
|
http_uri_fix_host(uri, http_query_uri(ctx->query), NULL);
|
|
|
|
result.data.location = str_dup(http_uri_str(uri));
|
|
http_uri_free(uri);
|
|
|
|
result.next = PROTO_OP_LOAD;
|
|
|
|
return result;
|
|
|
|
ERROR:
|
|
result.next = PROTO_OP_FINISH;
|
|
result.status = SANE_STATUS_IO_ERROR;
|
|
result.err = err;
|
|
return result;
|
|
}
|
|
|
|
/* Initiate image downloading
|
|
*/
|
|
static http_query*
|
|
escl_load_query (const proto_ctx *ctx)
|
|
{
|
|
char *url, *sep;
|
|
http_query *q;
|
|
|
|
sep = str_has_suffix(ctx->location, "/") ? "" : "/";
|
|
url = str_concat(ctx->location, sep, "NextDocument", NULL);
|
|
|
|
q = escl_http_get(ctx, url);
|
|
mem_free(url);
|
|
|
|
return q;
|
|
}
|
|
|
|
/* Decode result of image request
|
|
*/
|
|
static proto_result
|
|
escl_load_decode (const proto_ctx *ctx)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
proto_result result = {0};
|
|
error err = NULL;
|
|
timestamp t = 0;
|
|
|
|
/* Check HTTP status */
|
|
err = http_query_error(ctx->query);
|
|
if (err != NULL) {
|
|
if (ctx->params.src == ID_SOURCE_PLATEN && ctx->images_received > 0) {
|
|
result.next = PROTO_OP_CLEANUP;
|
|
if (escl->quirk_skip_cleanup) {
|
|
result.next = PROTO_OP_FINISH;
|
|
}
|
|
} else {
|
|
result.next = PROTO_OP_CHECK;
|
|
result.err = eloop_eprintf("HTTP: %s", ESTRING(err));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Compute delay until next load */
|
|
if (escl->quirk_next_load_delay && ctx->params.src != ID_SOURCE_PLATEN) {
|
|
t = timestamp_now() - http_query_timestamp(ctx->query);
|
|
t *= ESCL_NEXT_LOAD_DELAY_MAX;
|
|
|
|
if (t > ESCL_NEXT_LOAD_DELAY) {
|
|
t = ESCL_NEXT_LOAD_DELAY;
|
|
}
|
|
}
|
|
|
|
/* Fill proto_result */
|
|
result.next = PROTO_OP_LOAD;
|
|
result.delay = (int) t;
|
|
result.data.image = http_data_ref(http_query_get_response_data(ctx->query));
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Request device status
|
|
*/
|
|
static http_query*
|
|
escl_status_query (const proto_ctx *ctx)
|
|
{
|
|
return escl_http_get(ctx, "ScannerStatus");
|
|
}
|
|
|
|
/* Parse ScannerStatus response.
|
|
*
|
|
* Returned SANE_STATUS_UNSUPPORTED means status not understood
|
|
*/
|
|
static error
|
|
escl_parse_scanner_status (const proto_ctx *ctx,
|
|
const char *xml_text, size_t xml_len, escl_scanner_status *out)
|
|
{
|
|
error err = NULL;
|
|
xml_rd *xml;
|
|
const char *opname = proto_op_name(ctx->op);
|
|
escl_scanner_status sts = {SANE_STATUS_UNSUPPORTED,
|
|
SANE_STATUS_UNSUPPORTED};
|
|
|
|
/* Decode XML */
|
|
err = xml_rd_begin(&xml, xml_text, xml_len, NULL);
|
|
if (err != NULL) {
|
|
goto DONE;
|
|
}
|
|
|
|
if (!xml_rd_node_name_match(xml, "scan:ScannerStatus")) {
|
|
err = ERROR("XML: missed scan:ScannerStatus");
|
|
goto DONE;
|
|
}
|
|
|
|
xml_rd_enter(xml);
|
|
for (; !xml_rd_end(xml); xml_rd_next(xml)) {
|
|
if (xml_rd_node_name_match(xml, "pwg:State")) {
|
|
const char *state = xml_rd_node_value(xml);
|
|
if (!strcmp(state, "Idle")) {
|
|
sts.device_status = SANE_STATUS_GOOD;
|
|
} else if (!strcmp(state, "Processing")) {
|
|
sts.device_status = SANE_STATUS_DEVICE_BUSY;
|
|
} else if (!strcmp(state, "Testing")) {
|
|
/* HP LaserJet MFP M630 warm up */
|
|
sts.device_status = SANE_STATUS_DEVICE_BUSY;
|
|
} else {
|
|
sts.device_status = SANE_STATUS_UNSUPPORTED;
|
|
}
|
|
} else if (xml_rd_node_name_match(xml, "scan:AdfState")) {
|
|
const char *state = xml_rd_node_value(xml);
|
|
if (!strcmp(state, "ScannerAdfLoaded")) {
|
|
sts.adf_status = SANE_STATUS_GOOD;
|
|
} else if (!strcmp(state, "ScannerAdfJam")) {
|
|
sts.adf_status = SANE_STATUS_JAMMED;
|
|
} else if (!strcmp(state, "ScannerAdfDoorOpen")) {
|
|
sts.adf_status = SANE_STATUS_COVER_OPEN;
|
|
} else if (!strcmp(state, "ScannerAdfProcessing")) {
|
|
/* Kyocera version */
|
|
sts.adf_status = SANE_STATUS_NO_DOCS;
|
|
} else if (!strcmp(state, "ScannerAdfEmpty")) {
|
|
/* Cannon TR4500, EPSON XP-7100 */
|
|
sts.adf_status = SANE_STATUS_NO_DOCS;
|
|
} else {
|
|
sts.adf_status = SANE_STATUS_UNSUPPORTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
DONE:
|
|
xml_rd_finish(&xml);
|
|
|
|
if (err != NULL) {
|
|
log_debug(ctx->log, "%s: %s", opname, ESTRING(err));
|
|
} else {
|
|
log_debug(ctx->log, "%s: device status: %s",
|
|
opname, sane_strstatus(sts.device_status));
|
|
log_debug(ctx->log, "%s: ADF status: %s",
|
|
opname, sane_strstatus(sts.adf_status));
|
|
}
|
|
|
|
*out = sts;
|
|
return err;
|
|
}
|
|
|
|
/* Parse ScannerStatus response.
|
|
*
|
|
* Returned SANE_STATUS_UNSUPPORTED means status not understood
|
|
*/
|
|
static SANE_Status
|
|
escl_decode_scanner_status (const proto_ctx *ctx,
|
|
const char *xml_text, size_t xml_len)
|
|
{
|
|
escl_scanner_status sts;
|
|
error err;
|
|
SANE_Status status;
|
|
const char *opname = proto_op_name(ctx->op);
|
|
|
|
err = escl_parse_scanner_status(ctx, xml_text, xml_len, &sts);
|
|
if (err != NULL) {
|
|
return SANE_STATUS_IO_ERROR;
|
|
}
|
|
|
|
/* Decode Job status */
|
|
if (ctx->params.src != ID_SOURCE_PLATEN &&
|
|
sts.adf_status != SANE_STATUS_GOOD &&
|
|
sts.adf_status != SANE_STATUS_UNSUPPORTED) {
|
|
status = sts.adf_status;
|
|
} else {
|
|
status = sts.device_status;
|
|
}
|
|
|
|
log_debug(ctx->log, "%s: job status: %s", opname, sane_strstatus(status));
|
|
|
|
return status;
|
|
}
|
|
|
|
/* Decode result of device status request
|
|
*/
|
|
static proto_result
|
|
escl_status_decode (const proto_ctx *ctx)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) ctx->proto;
|
|
proto_result result = {0};
|
|
error err = NULL;
|
|
SANE_Status status;
|
|
int max_attempts;
|
|
bool temporary = false;
|
|
|
|
/* Decode status */
|
|
err = http_query_error(ctx->query);
|
|
if (err != NULL) {
|
|
status = SANE_STATUS_IO_ERROR;
|
|
goto FAIL;
|
|
} else {
|
|
http_data *data = http_query_get_response_data(ctx->query);
|
|
status = escl_decode_scanner_status(ctx, data->bytes, data->size);
|
|
}
|
|
|
|
/* Now it's time to make a decision */
|
|
max_attempts = ESCL_RETRY_ATTEMPTS;
|
|
if (ctx->failed_op == PROTO_OP_LOAD) {
|
|
max_attempts = ESCL_RETRY_ATTEMPTS_LOAD;
|
|
}
|
|
|
|
switch (ctx->failed_http_status) {
|
|
case HTTP_STATUS_SERVICE_UNAVAILABLE:
|
|
temporary = true;
|
|
break;
|
|
|
|
case HTTP_STATUS_NOT_FOUND:
|
|
if (escl->quirk_retry_on_404) {
|
|
temporary = true;
|
|
}
|
|
break;
|
|
|
|
case HTTP_STATUS_GONE:
|
|
if (escl->quirk_retry_on_410) {
|
|
temporary = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (temporary && ctx->failed_attempt < max_attempts) {
|
|
|
|
/* Note, some devices may return HTTP 503 error core, meaning
|
|
* that it makes sense to come back after small delay
|
|
*
|
|
* So if status doesn't cleanly indicate any error, lets retry
|
|
* several times
|
|
*/
|
|
bool retry = false;
|
|
|
|
switch (status) {
|
|
case SANE_STATUS_GOOD:
|
|
case SANE_STATUS_UNSUPPORTED:
|
|
case SANE_STATUS_DEVICE_BUSY:
|
|
retry = true;
|
|
break;
|
|
|
|
case SANE_STATUS_NO_DOCS:
|
|
/* For some devices SANE_STATUS_NO_DOCS is not
|
|
* reliable, if we have reached SANE_STATUS_NO_DOCS
|
|
* operation: HTTP 503 may mean "I'm temporary not
|
|
* ready, please try again", while ADF sensor
|
|
* raises "ADF empty" signal.
|
|
*
|
|
* So retry at this case
|
|
*/
|
|
if (ctx->failed_op == PROTO_OP_LOAD) {
|
|
retry = true;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (retry) {
|
|
result.next = ctx->failed_op;
|
|
result.delay = ESCL_RETRY_PAUSE;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/* If status cannot be cleanly decoded, look to HTTP status */
|
|
if (status == SANE_STATUS_GOOD || status == SANE_STATUS_UNSUPPORTED) {
|
|
status = SANE_STATUS_IO_ERROR;
|
|
switch (ctx->failed_http_status) {
|
|
case HTTP_STATUS_SERVICE_UNAVAILABLE:
|
|
status = SANE_STATUS_DEVICE_BUSY;
|
|
break;
|
|
|
|
case HTTP_STATUS_NOT_FOUND:
|
|
if (ctx->params.src != ID_SOURCE_PLATEN &&
|
|
ctx->failed_op == PROTO_OP_LOAD) {
|
|
status = SANE_STATUS_NO_DOCS;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Fill the result */
|
|
FAIL:
|
|
result.next = ctx->location ? PROTO_OP_CLEANUP : PROTO_OP_FINISH;
|
|
if (escl->quirk_skip_cleanup) {
|
|
result.next = PROTO_OP_FINISH;
|
|
}
|
|
|
|
result.status = status;
|
|
result.err = err;
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Cancel scan in progress
|
|
*/
|
|
static http_query*
|
|
escl_cancel_query (const proto_ctx *ctx)
|
|
{
|
|
return escl_http_query(ctx, ctx->location, "DELETE", NULL);
|
|
}
|
|
|
|
/******************** Test interfaces ********************/
|
|
/* Test interface: decode device capabilities
|
|
*/
|
|
static error
|
|
escl_test_decode_devcaps (proto_handler *proto,
|
|
const void *xml_text, size_t xms_size,
|
|
devcaps *caps)
|
|
{
|
|
proto_handler_escl *escl = (proto_handler_escl*) proto;
|
|
|
|
return escl_devcaps_parse(escl, caps, xml_text, xms_size);
|
|
}
|
|
|
|
/******************** Constructor/destructor ********************/
|
|
/* Free ESCL protocol handler
|
|
*/
|
|
static void
|
|
escl_free (proto_handler *proto)
|
|
{
|
|
mem_free(proto);
|
|
}
|
|
|
|
/* proto_handler_escl_new creates new eSCL protocol handler
|
|
*/
|
|
proto_handler*
|
|
proto_handler_escl_new (void)
|
|
{
|
|
proto_handler_escl *escl = mem_new(proto_handler_escl, 1);
|
|
|
|
escl->proto.name = "eSCL";
|
|
escl->proto.free = escl_free;
|
|
|
|
escl->proto.devcaps_query = escl_devcaps_query;
|
|
escl->proto.devcaps_decode = escl_devcaps_decode;
|
|
|
|
escl->proto.precheck_query = escl_precheck_query;
|
|
escl->proto.precheck_decode = escl_precheck_decode;
|
|
|
|
escl->proto.scan_query = escl_scan_query;
|
|
escl->proto.scan_decode = escl_scan_decode;
|
|
|
|
escl->proto.load_query = escl_load_query;
|
|
escl->proto.load_decode = escl_load_decode;
|
|
|
|
escl->proto.status_query = escl_status_query;
|
|
escl->proto.status_decode = escl_status_decode;
|
|
|
|
escl->proto.cleanup_query = escl_cancel_query;
|
|
escl->proto.cancel_query = escl_cancel_query;
|
|
|
|
escl->proto.test_decode_devcaps = escl_test_decode_devcaps;
|
|
|
|
return &escl->proto;
|
|
}
|
|
|
|
/* vim:ts=8:sw=4:et
|
|
*/
|