scanlib 1.3.0__tar.gz → 1.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scanlib-1.3.0/src/scanlib.egg-info → scanlib-1.3.1}/PKG-INFO +1 -1
- {scanlib-1.3.0 → scanlib-1.3.1}/pyproject.toml +1 -1
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/__init__.py +2 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/_types.py +18 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/backends/_escl.py +54 -5
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/backends/_macos.py +190 -89
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/backends/_sane.py +39 -12
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/backends/_wia.py +108 -45
- {scanlib-1.3.0 → scanlib-1.3.1/src/scanlib.egg-info}/PKG-INFO +1 -1
- {scanlib-1.3.0 → scanlib-1.3.1}/LICENSE +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/README.md +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/setup.cfg +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/setup.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/accel/_scanlib_accel.c +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/__main__.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/_jpeg.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/_mdns.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib/backends/__init__.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib.egg-info/SOURCES.txt +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib.egg-info/dependency_links.txt +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib.egg-info/entry_points.txt +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib.egg-info/requires.txt +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/src/scanlib.egg-info/top_level.txt +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_composite.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_hardware.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_jpeg.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_mdns.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_pdf.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_resolve.py +0 -0
- {scanlib-1.3.0 → scanlib-1.3.1}/tests/test_types.py +0 -0
|
@@ -26,6 +26,7 @@ from ._types import (
|
|
|
26
26
|
ScannerBusyError,
|
|
27
27
|
ScannerDefaults,
|
|
28
28
|
ScannerNotOpenError,
|
|
29
|
+
ScannerUnavailableError,
|
|
29
30
|
ScanOptions,
|
|
30
31
|
ScanSource,
|
|
31
32
|
ScannedDocument,
|
|
@@ -54,6 +55,7 @@ __all__ = [
|
|
|
54
55
|
"ScanError",
|
|
55
56
|
"ScanAborted",
|
|
56
57
|
"ScannerBusyError",
|
|
58
|
+
"ScannerUnavailableError",
|
|
57
59
|
"FeederEmptyError",
|
|
58
60
|
"NoScannerFoundError",
|
|
59
61
|
"BackendNotAvailableError",
|
|
@@ -47,6 +47,24 @@ class ScannerBusyError(ScanError):
|
|
|
47
47
|
super().__init__(message)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
class ScannerUnavailableError(ScanError):
|
|
51
|
+
"""The scanner could not be reached (offline, asleep, or disconnected).
|
|
52
|
+
|
|
53
|
+
Unlike :class:`ScannerBusyError`, the device is not held by another
|
|
54
|
+
session — it simply did not respond. This is often transient (a network
|
|
55
|
+
scanner that has gone to sleep) and worth retrying once the device is
|
|
56
|
+
reachable again.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_DEFAULT = (
|
|
60
|
+
"Scanner is unavailable — it may be offline, asleep, or disconnected. "
|
|
61
|
+
"Check that it is powered on and reachable, then try again."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __init__(self, message: str = _DEFAULT) -> None:
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
|
|
67
|
+
|
|
50
68
|
class ScannerNotOpenError(ScanLibError):
|
|
51
69
|
"""Operation requires an open scanner session."""
|
|
52
70
|
|
|
@@ -27,14 +27,17 @@ from collections.abc import Iterator
|
|
|
27
27
|
from urllib.parse import urlparse
|
|
28
28
|
|
|
29
29
|
from .._jpeg import decode_jpeg
|
|
30
|
-
from .._mdns import
|
|
30
|
+
from .._mdns import discover_escl_services
|
|
31
31
|
from .._types import (
|
|
32
32
|
DISCOVERY_TIMEOUT,
|
|
33
33
|
ColorMode,
|
|
34
|
+
FeederEmptyError,
|
|
34
35
|
ScanAborted,
|
|
35
36
|
ScanArea,
|
|
36
37
|
ScanError,
|
|
38
|
+
ScanLibError,
|
|
37
39
|
ScannerBusyError,
|
|
40
|
+
ScannerUnavailableError,
|
|
38
41
|
ScannedPage,
|
|
39
42
|
Scanner,
|
|
40
43
|
ScannerDefaults,
|
|
@@ -156,6 +159,26 @@ class _EsclConnection:
|
|
|
156
159
|
state_el = root.find(_ns(_SCAN_NS, "State"))
|
|
157
160
|
return state_el.text if state_el is not None and state_el.text else "Unknown"
|
|
158
161
|
|
|
162
|
+
def get_adf_state(self) -> str | None:
|
|
163
|
+
"""Return the feeder (ADF) state from ScannerStatus, or ``None``.
|
|
164
|
+
|
|
165
|
+
eSCL reports ``<scan:AdfState>`` as e.g. ``ScannerAdfProcessing``,
|
|
166
|
+
``ScannerAdfLoaded``, or ``ScannerAdfEmpty``. Returns ``None`` when
|
|
167
|
+
the scanner doesn't report it (best-effort — not all models do).
|
|
168
|
+
"""
|
|
169
|
+
conn = self._connect()
|
|
170
|
+
conn.request("GET", f"{self.base_path}/ScannerStatus")
|
|
171
|
+
resp = conn.getresponse()
|
|
172
|
+
body = resp.read()
|
|
173
|
+
if resp.status != 200:
|
|
174
|
+
return None
|
|
175
|
+
try:
|
|
176
|
+
root = ET.fromstring(body)
|
|
177
|
+
except ET.ParseError:
|
|
178
|
+
return None
|
|
179
|
+
el = next(root.iter(_ns(_SCAN_NS, "AdfState")), None)
|
|
180
|
+
return el.text.strip() if el is not None and el.text else None
|
|
181
|
+
|
|
159
182
|
def cancel_active_jobs(self) -> int:
|
|
160
183
|
"""Cancel any active jobs on the scanner. Returns count cancelled."""
|
|
161
184
|
conn = self._connect()
|
|
@@ -186,7 +209,6 @@ class _EsclConnection:
|
|
|
186
209
|
"""
|
|
187
210
|
import time
|
|
188
211
|
|
|
189
|
-
last_status = 0
|
|
190
212
|
for attempt in range(retries):
|
|
191
213
|
conn = self._connect()
|
|
192
214
|
conn.request(
|
|
@@ -197,7 +219,6 @@ class _EsclConnection:
|
|
|
197
219
|
)
|
|
198
220
|
resp = conn.getresponse()
|
|
199
221
|
resp.read() # drain body
|
|
200
|
-
last_status = resp.status
|
|
201
222
|
if resp.status in (409, 503):
|
|
202
223
|
if attempt < retries - 1:
|
|
203
224
|
# Try cancelling any stale jobs before retrying
|
|
@@ -570,11 +591,10 @@ def _decode_png(data: bytes) -> tuple[bytes, int, int, int]:
|
|
|
570
591
|
raise ValueError(f"Unsupported PNG color type: {color_type}")
|
|
571
592
|
|
|
572
593
|
row_bytes = width * components
|
|
573
|
-
#
|
|
594
|
+
# Skip the per-row filter byte (decoder assumes filter type 0 / None).
|
|
574
595
|
pixels = bytearray()
|
|
575
596
|
for y in range(height):
|
|
576
597
|
offset = y * (row_bytes + 1)
|
|
577
|
-
_filter_byte = raw_filtered[offset]
|
|
578
598
|
pixels.extend(raw_filtered[offset + 1 : offset + 1 + row_bytes])
|
|
579
599
|
|
|
580
600
|
return bytes(pixels), width, height, components
|
|
@@ -732,6 +752,17 @@ class EsclBackend:
|
|
|
732
752
|
|
|
733
753
|
try:
|
|
734
754
|
caps_xml = conn.get_capabilities()
|
|
755
|
+
except ScanLibError:
|
|
756
|
+
# Already a typed scanlib error (busy/unavailable/…) — preserve it.
|
|
757
|
+
raise
|
|
758
|
+
except OSError as exc:
|
|
759
|
+
# Socket-level failure: host unreachable, connection refused, or
|
|
760
|
+
# timed out — the scanner is offline, asleep, or disconnected.
|
|
761
|
+
raise ScannerUnavailableError(
|
|
762
|
+
f"Scanner at {conn.ip}:{conn.port} is unavailable — it may be "
|
|
763
|
+
"offline, asleep, or disconnected. Check that it is powered on "
|
|
764
|
+
"and reachable, then try again."
|
|
765
|
+
) from exc
|
|
735
766
|
except Exception as exc:
|
|
736
767
|
raise ScanError(f"Failed to get scanner capabilities: {exc}") from exc
|
|
737
768
|
|
|
@@ -769,6 +800,16 @@ class EsclBackend:
|
|
|
769
800
|
|
|
770
801
|
check_progress(options.progress, 0)
|
|
771
802
|
|
|
803
|
+
# For the feeder, check the ADF state before creating a job. A
|
|
804
|
+
# scanner asked to scan from an empty feeder would otherwise
|
|
805
|
+
# physically engage the ADF and, on some models, keep producing
|
|
806
|
+
# (blank) pages instead of cleanly reporting "no documents". This
|
|
807
|
+
# is best-effort: only an explicit "empty" report short-circuits;
|
|
808
|
+
# scanners that don't report AdfState fall through to the job (and
|
|
809
|
+
# the page_num == 0 check below still catches an empty feeder).
|
|
810
|
+
if is_feeder and conn.get_adf_state() == "ScannerAdfEmpty":
|
|
811
|
+
raise FeederEmptyError("No documents in feeder")
|
|
812
|
+
|
|
772
813
|
try:
|
|
773
814
|
job_path = conn.create_job(settings_xml)
|
|
774
815
|
except ScanError:
|
|
@@ -812,6 +853,14 @@ class EsclBackend:
|
|
|
812
853
|
if not is_feeder:
|
|
813
854
|
break # flatbed: one page per job
|
|
814
855
|
|
|
856
|
+
if page_num == 0:
|
|
857
|
+
# A feeder job that produced nothing means the feeder was
|
|
858
|
+
# empty; a flatbed job with no data is a scan failure. Match
|
|
859
|
+
# the SANE/macOS/WIA backends rather than yielding nothing.
|
|
860
|
+
if is_feeder:
|
|
861
|
+
raise FeederEmptyError("No documents in feeder")
|
|
862
|
+
raise ScanError("Scan completed but no image data was received")
|
|
863
|
+
|
|
815
864
|
except (ScanAborted, ScanError):
|
|
816
865
|
conn.delete_job(job_path)
|
|
817
866
|
raise
|
|
@@ -24,6 +24,7 @@ from .._types import (
|
|
|
24
24
|
ScanAborted,
|
|
25
25
|
ScanError,
|
|
26
26
|
ScannerBusyError,
|
|
27
|
+
ScannerUnavailableError,
|
|
27
28
|
ScannedPage,
|
|
28
29
|
Scanner,
|
|
29
30
|
ScannerDefaults,
|
|
@@ -51,12 +52,38 @@ _COLOR_MODE_TO_PIXEL_DATA_TYPE = {
|
|
|
51
52
|
# ICScannerTransferMode
|
|
52
53
|
_TRANSFER_MODE_MEMORY_BASED = 1
|
|
53
54
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
# ImageCaptureCore status codes (signed 32-bit), from ICReturnCodes.h. All
|
|
56
|
+
# error classification is done by code, never by parsing the (localised,
|
|
57
|
+
# vendor-specific) NSError message string.
|
|
58
|
+
_FBSY_ERR = -47 # classic macOS fBsyErr (resource busy)
|
|
59
|
+
_IC_SCAN_OPERATION_CANCELED = -9924 # ICReturnScanOperationCanceled
|
|
60
|
+
_IC_SCANNER_IN_USE_BY_LOCAL_USER = -9925 # ICReturnScannerInUseByLocalUser
|
|
61
|
+
_IC_SCANNER_IN_USE_BY_REMOTE_USER = -9926 # ICReturnScannerInUseByRemoteUser
|
|
62
|
+
# Legacy ICLegacyReturnCode* communication-layer failures.
|
|
63
|
+
_IC_LEGACY_COMMUNICATION_ERR = -9900 # ICLegacyReturnCodeCommunicationErr
|
|
64
|
+
_IC_LEGACY_DEVICE_NOT_FOUND_ERR = -9901 # ICLegacyReturnCodeDeviceNotFoundErr
|
|
65
|
+
_IC_LEGACY_DEVICE_NOT_OPEN_ERR = -9902 # ICLegacyReturnCodeDeviceNotOpenErr
|
|
66
|
+
|
|
67
|
+
# Device is already in use by another session/application and cannot be opened.
|
|
68
|
+
# -47 is what real network scanners (e.g. Brother) return when another app holds
|
|
69
|
+
# the session; -9925/-9926 are ImageCaptureCore's documented in-use values.
|
|
70
|
+
_BUSY_ERROR_CODES = frozenset(
|
|
71
|
+
{_FBSY_ERR, _IC_SCANNER_IN_USE_BY_LOCAL_USER, _IC_SCANNER_IN_USE_BY_REMOTE_USER}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# The device object exists (it was discovered) but the hardware can't be
|
|
75
|
+
# reached — offline, asleep, or disconnected. Distinct from busy: the device
|
|
76
|
+
# isn't held by anyone, it just didn't answer.
|
|
77
|
+
_UNAVAILABLE_ERROR_CODES = frozenset(
|
|
78
|
+
{
|
|
79
|
+
_IC_LEGACY_COMMUNICATION_ERR,
|
|
80
|
+
_IC_LEGACY_DEVICE_NOT_FOUND_ERR,
|
|
81
|
+
_IC_LEGACY_DEVICE_NOT_OPEN_ERR,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Scan was cancelled (by the user at the device, or programmatically).
|
|
86
|
+
_CANCEL_ERROR_CODES = frozenset({_IC_SCAN_OPERATION_CANCELED})
|
|
60
87
|
|
|
61
88
|
|
|
62
89
|
def _normalize_error_code(code: int | None) -> int | None:
|
|
@@ -74,6 +101,14 @@ def _normalize_error_code(code: int | None) -> int | None:
|
|
|
74
101
|
return code
|
|
75
102
|
|
|
76
103
|
|
|
104
|
+
def _error_code(error) -> int | None:
|
|
105
|
+
"""Return the normalized signed status code of an NSError, or None."""
|
|
106
|
+
try:
|
|
107
|
+
return _normalize_error_code(int(error.code()))
|
|
108
|
+
except Exception:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
77
112
|
def pump_run_loop(duration: float) -> None:
|
|
78
113
|
"""Run the current thread's run loop for up to *duration* seconds.
|
|
79
114
|
|
|
@@ -197,10 +232,7 @@ class _ScanDelegate(NSObject):
|
|
|
197
232
|
def device_didOpenSessionWithError_(self, device, error):
|
|
198
233
|
if error:
|
|
199
234
|
self.error = str(error)
|
|
200
|
-
|
|
201
|
-
self.error_code = _normalize_error_code(int(error.code()))
|
|
202
|
-
except Exception:
|
|
203
|
-
self.error_code = None
|
|
235
|
+
self.error_code = _error_code(error)
|
|
204
236
|
else:
|
|
205
237
|
self.session_open = True
|
|
206
238
|
self._open_event.set()
|
|
@@ -238,6 +270,7 @@ class _ScanDelegate(NSObject):
|
|
|
238
270
|
def scannerDevice_didCompleteScanWithError_(self, device, error):
|
|
239
271
|
if error:
|
|
240
272
|
self.error = str(error)
|
|
273
|
+
self.error_code = _error_code(error)
|
|
241
274
|
self._scan_done.set()
|
|
242
275
|
|
|
243
276
|
def device_didCloseSessionWithError_(self, device, error):
|
|
@@ -439,6 +472,34 @@ def _safe_str(dev, attr: str) -> str | None:
|
|
|
439
472
|
return None
|
|
440
473
|
|
|
441
474
|
|
|
475
|
+
def _name_and_location(dev) -> tuple[str | None, str | None]:
|
|
476
|
+
"""Compute the (display name, location) pair for an ICDevice.
|
|
477
|
+
|
|
478
|
+
ImageCaptureCore's ``locationDescription`` is *not* a physical location
|
|
479
|
+
for network scanners: over TCP/IP it returns the device's Bonjour name —
|
|
480
|
+
the label macOS itself shows (e.g. ``"PLC"``, ``"Piano 1°"``) — while
|
|
481
|
+
``name`` is only the generic model series (e.g. ``"EPSON … Series"``).
|
|
482
|
+
So for network devices the location string is really the display name;
|
|
483
|
+
surface it as such and leave ``location`` unset rather than echoing the
|
|
484
|
+
same value in both fields.
|
|
485
|
+
|
|
486
|
+
For USB/FireWire/etc. devices ``locationDescription`` is the standard bus
|
|
487
|
+
descriptor (``"USB"``, …) — a genuine location — so keep it as the
|
|
488
|
+
location and use the device name as the display name.
|
|
489
|
+
"""
|
|
490
|
+
name = _safe_str(dev, "name")
|
|
491
|
+
loc = _safe_str(dev, "locationDescription")
|
|
492
|
+
try:
|
|
493
|
+
transport = dev.transportType()
|
|
494
|
+
except Exception:
|
|
495
|
+
transport = None
|
|
496
|
+
if transport is not None and transport == ImageCaptureCore.ICTransportTypeTCPIP:
|
|
497
|
+
# Bonjour name: the OS-displayed name, not a location.
|
|
498
|
+
return (loc or name), None
|
|
499
|
+
# Bus-attached device: locationDescription is the bus type.
|
|
500
|
+
return name, (loc if loc and loc != name else None)
|
|
501
|
+
|
|
502
|
+
|
|
442
503
|
class MacOSBackend:
|
|
443
504
|
backend_name = "imagecapture"
|
|
444
505
|
"""macOS scanning backend using ImageCaptureCore.
|
|
@@ -627,27 +688,24 @@ class MacOSBackend:
|
|
|
627
688
|
if uid not in self._devices:
|
|
628
689
|
self._devices[uid] = dev
|
|
629
690
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
# Image Capture shows the device name verbatim.
|
|
647
|
-
display_name=dev.name(),
|
|
691
|
+
scanners = []
|
|
692
|
+
for dev in delegate.scanners:
|
|
693
|
+
display_name, location = _name_and_location(dev)
|
|
694
|
+
scanners.append(
|
|
695
|
+
Scanner(
|
|
696
|
+
name=dev.name(),
|
|
697
|
+
vendor=_safe_str(dev, "manufacturer"),
|
|
698
|
+
model=None,
|
|
699
|
+
backend=self.backend_name,
|
|
700
|
+
scanner_id=_safe_str(dev, "UUIDString") or dev.name(),
|
|
701
|
+
uuid=_safe_str(dev, "UUIDString") or None,
|
|
702
|
+
location=location,
|
|
703
|
+
# The label Image Capture shows (Bonjour name for
|
|
704
|
+
# network scanners, device name otherwise).
|
|
705
|
+
display_name=display_name,
|
|
706
|
+
)
|
|
648
707
|
)
|
|
649
|
-
|
|
650
|
-
]
|
|
708
|
+
return scanners
|
|
651
709
|
|
|
652
710
|
return self._on_main(_build_list)
|
|
653
711
|
|
|
@@ -691,6 +749,12 @@ class MacOSBackend:
|
|
|
691
749
|
"or host (e.g. Image Capture, Preview, or a vendor tool). "
|
|
692
750
|
"Close it and try again."
|
|
693
751
|
)
|
|
752
|
+
if last_error_code in _UNAVAILABLE_ERROR_CODES:
|
|
753
|
+
raise ScannerUnavailableError(
|
|
754
|
+
f"Scanner {scanner.name!r} is unavailable — it may be "
|
|
755
|
+
"offline, asleep, or disconnected. Check that it is powered "
|
|
756
|
+
"on and reachable, then try again."
|
|
757
|
+
)
|
|
694
758
|
raise ScanError(f"Failed to open device session: {last_error}")
|
|
695
759
|
|
|
696
760
|
if scan_delegate is None or not scan_delegate.session_open:
|
|
@@ -885,65 +949,9 @@ class MacOSBackend:
|
|
|
885
949
|
time.sleep(0.5)
|
|
886
950
|
|
|
887
951
|
is_feeder = options.source == ScanSource.FEEDER
|
|
888
|
-
all_pages
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
scan_delegate._scan_done.clear()
|
|
892
|
-
scan_delegate.error = None
|
|
893
|
-
scan_delegate.completed_pages = []
|
|
894
|
-
scan_delegate._current_bands = []
|
|
895
|
-
scan_delegate._rows_received = 0
|
|
896
|
-
scan_delegate._last_pct = 0
|
|
897
|
-
scan_delegate._aborted = False
|
|
898
|
-
scan_delegate._expected_height = expected_height
|
|
899
|
-
scan_delegate._progress = options.progress
|
|
900
|
-
|
|
901
|
-
check_progress(options.progress, -1)
|
|
902
|
-
|
|
903
|
-
self._on_main(device.requestScan)
|
|
904
|
-
|
|
905
|
-
# Poll with short timeouts so scanner.abort() is responsive.
|
|
906
|
-
while not scan_delegate._scan_done.wait(timeout=0.25):
|
|
907
|
-
if scanner._abort_event.is_set():
|
|
908
|
-
self._on_main(device.cancelScan)
|
|
909
|
-
raise ScanAborted("Scan aborted")
|
|
910
|
-
|
|
911
|
-
if scan_delegate._aborted:
|
|
912
|
-
self._on_main(device.cancelScan)
|
|
913
|
-
raise ScanAborted("Scan aborted by user")
|
|
914
|
-
|
|
915
|
-
if scan_delegate.error:
|
|
916
|
-
err_lower = scan_delegate.error.lower()
|
|
917
|
-
if "cancel" in err_lower or "abort" in err_lower:
|
|
918
|
-
raise ScanAborted(
|
|
919
|
-
f"Scan cancelled by device: {scan_delegate.error}"
|
|
920
|
-
)
|
|
921
|
-
raise ScanError(f"Scan failed: {scan_delegate.error}")
|
|
922
|
-
|
|
923
|
-
# Flush the last (or only) page
|
|
924
|
-
if scan_delegate._current_bands:
|
|
925
|
-
scan_delegate._finish_page()
|
|
926
|
-
|
|
927
|
-
if not scan_delegate.completed_pages:
|
|
928
|
-
if is_feeder:
|
|
929
|
-
raise FeederEmptyError("No documents in feeder")
|
|
930
|
-
raise ScanError("Scan completed but no image data was received")
|
|
931
|
-
|
|
932
|
-
for bands, w, h, bpc, nc, pdt in scan_delegate.completed_pages:
|
|
933
|
-
raw, width, height, mode = _assemble_image(
|
|
934
|
-
bands, w, h, bpc, nc, pdt
|
|
935
|
-
)
|
|
936
|
-
all_pages.append(
|
|
937
|
-
ScannedPage(
|
|
938
|
-
data=raw,
|
|
939
|
-
width=width,
|
|
940
|
-
height=height,
|
|
941
|
-
color_mode=mode,
|
|
942
|
-
)
|
|
943
|
-
)
|
|
944
|
-
|
|
945
|
-
if not is_feeder:
|
|
946
|
-
break
|
|
952
|
+
all_pages = self._collect_scan_rounds(
|
|
953
|
+
scanner, device, scan_delegate, options, is_feeder, expected_height
|
|
954
|
+
)
|
|
947
955
|
|
|
948
956
|
check_progress(options.progress, 100)
|
|
949
957
|
return all_pages
|
|
@@ -951,3 +959,96 @@ class MacOSBackend:
|
|
|
951
959
|
raise
|
|
952
960
|
except Exception as exc:
|
|
953
961
|
raise ScanError(f"Scan failed: {exc}") from exc
|
|
962
|
+
|
|
963
|
+
def _collect_scan_rounds(
|
|
964
|
+
self,
|
|
965
|
+
scanner: Scanner,
|
|
966
|
+
device,
|
|
967
|
+
scan_delegate,
|
|
968
|
+
options: ScanOptions,
|
|
969
|
+
is_feeder: bool,
|
|
970
|
+
expected_height: int,
|
|
971
|
+
) -> list[ScannedPage]:
|
|
972
|
+
"""Drive ``requestScan`` rounds and gather the resulting pages.
|
|
973
|
+
|
|
974
|
+
A flatbed scan runs exactly one round. A feeder keeps scanning until
|
|
975
|
+
a round yields no page: ImageCaptureCore signals the end of the stack
|
|
976
|
+
with an empty pass — or, on some devices, a "no documents in feeder"
|
|
977
|
+
completion error. Either way that is the normal terminator once at
|
|
978
|
+
least one page has been collected, so the run stops and returns the
|
|
979
|
+
pages instead of surfacing the condition as an error. Only an empty
|
|
980
|
+
*first* round means the feeder was genuinely empty.
|
|
981
|
+
"""
|
|
982
|
+
all_pages: list[ScannedPage] = []
|
|
983
|
+
|
|
984
|
+
while True:
|
|
985
|
+
scan_delegate._scan_done.clear()
|
|
986
|
+
scan_delegate.error = None
|
|
987
|
+
scan_delegate.error_code = None
|
|
988
|
+
scan_delegate.completed_pages = []
|
|
989
|
+
scan_delegate._current_bands = []
|
|
990
|
+
scan_delegate._rows_received = 0
|
|
991
|
+
scan_delegate._last_pct = 0
|
|
992
|
+
scan_delegate._aborted = False
|
|
993
|
+
scan_delegate._expected_height = expected_height
|
|
994
|
+
scan_delegate._progress = options.progress
|
|
995
|
+
|
|
996
|
+
check_progress(options.progress, -1)
|
|
997
|
+
|
|
998
|
+
self._on_main(device.requestScan)
|
|
999
|
+
|
|
1000
|
+
# Poll with short timeouts so scanner.abort() is responsive.
|
|
1001
|
+
while not scan_delegate._scan_done.wait(timeout=0.25):
|
|
1002
|
+
if scanner._abort_event.is_set():
|
|
1003
|
+
self._on_main(device.cancelScan)
|
|
1004
|
+
raise ScanAborted("Scan aborted")
|
|
1005
|
+
|
|
1006
|
+
if scan_delegate._aborted:
|
|
1007
|
+
self._on_main(device.cancelScan)
|
|
1008
|
+
raise ScanAborted("Scan aborted by user")
|
|
1009
|
+
|
|
1010
|
+
# Flush the last (or only) page and collect this round's pages
|
|
1011
|
+
# *before* interpreting any completion error: a feeder's final
|
|
1012
|
+
# pass can deliver a sheet and then report "no documents".
|
|
1013
|
+
if scan_delegate._current_bands:
|
|
1014
|
+
scan_delegate._finish_page()
|
|
1015
|
+
|
|
1016
|
+
new_pages = list(scan_delegate.completed_pages)
|
|
1017
|
+
for bands, w, h, bpc, nc, pdt in new_pages:
|
|
1018
|
+
raw, width, height, mode = _assemble_image(bands, w, h, bpc, nc, pdt)
|
|
1019
|
+
all_pages.append(
|
|
1020
|
+
ScannedPage(
|
|
1021
|
+
data=raw,
|
|
1022
|
+
width=width,
|
|
1023
|
+
height=height,
|
|
1024
|
+
color_mode=mode,
|
|
1025
|
+
)
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
if scan_delegate.error:
|
|
1029
|
+
if scan_delegate.error_code in _CANCEL_ERROR_CODES:
|
|
1030
|
+
raise ScanAborted(
|
|
1031
|
+
f"Scan cancelled by device: {scan_delegate.error}"
|
|
1032
|
+
)
|
|
1033
|
+
# ImageCaptureCore has no "feeder empty" code, so the pass
|
|
1034
|
+
# after the last sheet surfaces as a generic, vendor-specific
|
|
1035
|
+
# scanner error. Once we've collected at least one page that
|
|
1036
|
+
# is the normal end-of-feeder signal, not a failure — stop and
|
|
1037
|
+
# return what we scanned instead of discarding it.
|
|
1038
|
+
if is_feeder and all_pages:
|
|
1039
|
+
break
|
|
1040
|
+
raise ScanError(f"Scan failed: {scan_delegate.error}")
|
|
1041
|
+
|
|
1042
|
+
if not new_pages:
|
|
1043
|
+
# An empty pass ends a feeder run once pages exist; otherwise
|
|
1044
|
+
# the feeder was empty to begin with.
|
|
1045
|
+
if is_feeder:
|
|
1046
|
+
if all_pages:
|
|
1047
|
+
break
|
|
1048
|
+
raise FeederEmptyError("No documents in feeder")
|
|
1049
|
+
raise ScanError("Scan completed but no image data was received")
|
|
1050
|
+
|
|
1051
|
+
if not is_feeder:
|
|
1052
|
+
break
|
|
1053
|
+
|
|
1054
|
+
return all_pages
|
|
@@ -20,7 +20,9 @@ from .._types import (
|
|
|
20
20
|
ScanArea,
|
|
21
21
|
ScanAborted,
|
|
22
22
|
ScanError,
|
|
23
|
+
ScanLibError,
|
|
23
24
|
ScannerBusyError,
|
|
25
|
+
ScannerUnavailableError,
|
|
24
26
|
ScannedPage,
|
|
25
27
|
Scanner,
|
|
26
28
|
ScannerDefaults,
|
|
@@ -219,15 +221,38 @@ if _lib is not None:
|
|
|
219
221
|
# ---------------------------------------------------------------------------
|
|
220
222
|
|
|
221
223
|
|
|
224
|
+
def _status_message(status: int, context: str = "") -> str:
|
|
225
|
+
"""Format a human-readable message for a ``SANE_Status`` code."""
|
|
226
|
+
name = _STATUS_NAMES.get(status, f"unknown ({status})")
|
|
227
|
+
msg = f"SANE error: {name}"
|
|
228
|
+
if context:
|
|
229
|
+
msg = f"{context}: {msg}"
|
|
230
|
+
return msg
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _status_error(status: int, context: str = "") -> ScanError:
|
|
234
|
+
"""Build a :class:`ScanError` for *status*, tagged with ``.sane_status``.
|
|
235
|
+
|
|
236
|
+
Callers classify the failure by the numeric ``SANE_Status`` code on the
|
|
237
|
+
exception rather than by parsing the message text.
|
|
238
|
+
"""
|
|
239
|
+
err = ScanError(_status_message(status, context))
|
|
240
|
+
err.sane_status = status
|
|
241
|
+
return err
|
|
242
|
+
|
|
243
|
+
|
|
222
244
|
def _check_status(status: int, context: str = "") -> None:
|
|
223
245
|
if status != _STATUS_GOOD:
|
|
224
|
-
name = _STATUS_NAMES.get(status, f"unknown ({status})")
|
|
225
|
-
msg = f"SANE error: {name}"
|
|
226
|
-
if context:
|
|
227
|
-
msg = f"{context}: {msg}"
|
|
228
246
|
if status == _STATUS_DEVICE_BUSY:
|
|
229
247
|
raise ScannerBusyError()
|
|
230
|
-
|
|
248
|
+
if status == _STATUS_IO_ERROR:
|
|
249
|
+
# I/O error means communication with the device failed — it is
|
|
250
|
+
# offline, asleep, or disconnected, not held by another session.
|
|
251
|
+
raise ScannerUnavailableError(
|
|
252
|
+
f"{_status_message(status, context)} — the scanner may be "
|
|
253
|
+
"offline, asleep, or disconnected."
|
|
254
|
+
)
|
|
255
|
+
raise _status_error(status, context)
|
|
231
256
|
|
|
232
257
|
|
|
233
258
|
def _ensure_lib() -> None:
|
|
@@ -723,8 +748,7 @@ def _scan_one_page(dev: _SaneDevice, progress=None) -> ScannedPage:
|
|
|
723
748
|
if status != _STATUS_GOOD:
|
|
724
749
|
if status == _STATUS_DEVICE_BUSY:
|
|
725
750
|
raise ScannerBusyError()
|
|
726
|
-
|
|
727
|
-
raise ScanError(f"sane_start: {name}")
|
|
751
|
+
raise _status_error(status, "sane_start")
|
|
728
752
|
|
|
729
753
|
params = dev.get_parameters()
|
|
730
754
|
width = params.pixels_per_line
|
|
@@ -752,8 +776,7 @@ def _scan_one_page(dev: _SaneDevice, progress=None) -> ScannedPage:
|
|
|
752
776
|
# instead of EOF once all data has been delivered.
|
|
753
777
|
break
|
|
754
778
|
if st != _STATUS_GOOD:
|
|
755
|
-
|
|
756
|
-
raise ScanError(f"sane_read: {name}")
|
|
779
|
+
raise _status_error(st, "sane_read")
|
|
757
780
|
|
|
758
781
|
raw = b"".join(chunks)
|
|
759
782
|
|
|
@@ -872,6 +895,9 @@ class SaneBackend:
|
|
|
872
895
|
def open_scanner(self, scanner: Scanner) -> None:
|
|
873
896
|
try:
|
|
874
897
|
dev = _open_device(scanner.name)
|
|
898
|
+
except ScanLibError:
|
|
899
|
+
# Already a typed scanlib error (busy/unavailable/…) — preserve it.
|
|
900
|
+
raise
|
|
875
901
|
except Exception as exc:
|
|
876
902
|
raise ScanError(f"Failed to open scanner {scanner.name!r}: {exc}") from exc
|
|
877
903
|
self._handles[scanner.name] = dev
|
|
@@ -983,10 +1009,11 @@ class SaneBackend:
|
|
|
983
1009
|
scan_started = True
|
|
984
1010
|
page = _scan_one_page(dev, progress=options.progress)
|
|
985
1011
|
except ScanError as exc:
|
|
986
|
-
|
|
987
|
-
|
|
1012
|
+
status = getattr(exc, "sane_status", None)
|
|
1013
|
+
# End of an automatic feeder run: no more documents / EOF.
|
|
1014
|
+
if is_feeder and status in (_STATUS_NO_DOCS, _STATUS_EOF):
|
|
988
1015
|
break
|
|
989
|
-
if
|
|
1016
|
+
if status in (_STATUS_CANCELLED, _STATUS_JAMMED):
|
|
990
1017
|
raise ScanAborted(f"Scan cancelled by device: {exc}") from exc
|
|
991
1018
|
raise
|
|
992
1019
|
|
|
@@ -66,6 +66,7 @@ from .._types import (
|
|
|
66
66
|
ScanAborted,
|
|
67
67
|
ScanError,
|
|
68
68
|
ScannerBusyError,
|
|
69
|
+
ScannerUnavailableError,
|
|
69
70
|
ScannedPage,
|
|
70
71
|
Scanner,
|
|
71
72
|
ScannerDefaults,
|
|
@@ -115,11 +116,28 @@ _FEEDER = 2
|
|
|
115
116
|
|
|
116
117
|
_WIA_FORMAT_BMP = GUID("{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}")
|
|
117
118
|
|
|
119
|
+
# WIA item categories (wiadef.h). A WIA 2.0 scanner exposes one child
|
|
120
|
+
# item per source; scanning the *wrong* item (e.g. the flatbed item with
|
|
121
|
+
# the feeder selected) makes some drivers scan blank pages from an empty
|
|
122
|
+
# feeder forever instead of reporting WIA_ERROR_PAPER_EMPTY.
|
|
123
|
+
_WIA_CATEGORY_FLATBED = GUID("{FB607B1F-43F3-488B-855B-FB703EC342A6}")
|
|
124
|
+
_WIA_CATEGORY_FEEDER = GUID("{FE131934-F84C-42AD-8DA4-6129CDDD7288}")
|
|
125
|
+
|
|
118
126
|
_WIA_ERROR_PAPER_EMPTY = -2145320957 # 0x80210003
|
|
119
127
|
# HRESULTs that mean the device is in use / unavailable to this session.
|
|
120
128
|
_WIA_ERROR_BUSY = -2145320954 # 0x80210006
|
|
121
129
|
_WIA_ERROR_DEVICE_LOCKED = -2145320947 # 0x8021000D
|
|
122
130
|
_WIA_BUSY_ERRORS = frozenset({_WIA_ERROR_BUSY, _WIA_ERROR_DEVICE_LOCKED})
|
|
131
|
+
# HRESULTs that mean the device couldn't be reached — offline, asleep, or
|
|
132
|
+
# disconnected — rather than held by another session.
|
|
133
|
+
_WIA_ERROR_OFFLINE = -2145320955 # 0x80210005
|
|
134
|
+
_WIA_ERROR_DEVICE_COMM = -2145320950 # 0x8021000A (DEVICE_COMMUNICATION)
|
|
135
|
+
_WIA_UNAVAILABLE_ERRORS = frozenset({_WIA_ERROR_OFFLINE, _WIA_ERROR_DEVICE_COMM})
|
|
136
|
+
# Generic Windows cancellation HRESULTs (a device-initiated cancel; a
|
|
137
|
+
# caller-initiated abort is detected separately via the callback).
|
|
138
|
+
_E_ABORT = -2147467260 # 0x80004004
|
|
139
|
+
_WIA_ERROR_CANCELLED = -2147023673 # 0x800704C7 HRESULT_FROM_WIN32(ERROR_CANCELLED)
|
|
140
|
+
_WIA_CANCEL_ERRORS = frozenset({_E_ABORT, _WIA_ERROR_CANCELLED})
|
|
123
141
|
|
|
124
142
|
# Property attribute flags (from WiaDef.h)
|
|
125
143
|
_WIA_PROP_RANGE = 0x10
|
|
@@ -780,6 +798,42 @@ def _read_wia_color_modes(storage) -> list[ColorMode]:
|
|
|
780
798
|
return []
|
|
781
799
|
|
|
782
800
|
|
|
801
|
+
def _enum_scan_items(root_item):
|
|
802
|
+
"""Map scan sources to their dedicated WIA child items.
|
|
803
|
+
|
|
804
|
+
Returns ``(child_map, default_child)`` where *child_map* maps
|
|
805
|
+
:class:`ScanSource` to the child item whose WIA category matches that
|
|
806
|
+
source, and *default_child* is the first child item (a fallback for
|
|
807
|
+
scanners that expose a single generic item). Scanning a source through
|
|
808
|
+
its own child item is what makes the driver report
|
|
809
|
+
``WIA_ERROR_PAPER_EMPTY`` for an empty feeder instead of scanning blanks.
|
|
810
|
+
"""
|
|
811
|
+
child_map: dict[ScanSource, object] = {}
|
|
812
|
+
default_child = None
|
|
813
|
+
try:
|
|
814
|
+
enum_items = root_item.EnumChildItems(None)
|
|
815
|
+
except Exception:
|
|
816
|
+
return child_map, default_child
|
|
817
|
+
while True:
|
|
818
|
+
try:
|
|
819
|
+
child, fetched = enum_items.Next(1)
|
|
820
|
+
except Exception:
|
|
821
|
+
break
|
|
822
|
+
if not fetched:
|
|
823
|
+
break
|
|
824
|
+
if default_child is None:
|
|
825
|
+
default_child = child
|
|
826
|
+
try:
|
|
827
|
+
category = child.GetItemCategory()
|
|
828
|
+
except Exception:
|
|
829
|
+
continue
|
|
830
|
+
if category == _WIA_CATEGORY_FEEDER:
|
|
831
|
+
child_map[ScanSource.FEEDER] = child
|
|
832
|
+
elif category == _WIA_CATEGORY_FLATBED:
|
|
833
|
+
child_map[ScanSource.FLATBED] = child
|
|
834
|
+
return child_map, default_child
|
|
835
|
+
|
|
836
|
+
|
|
783
837
|
def _read_wia_sources(storage) -> list[ScanSource]:
|
|
784
838
|
"""Determine available scan sources from device capabilities."""
|
|
785
839
|
caps = _read_prop(storage, _WIA_DPS_DOCUMENT_HANDLING_CAPABILITIES, 0)
|
|
@@ -1149,8 +1203,15 @@ class WiaBackend:
|
|
|
1149
1203
|
dm = self._create_device_manager()
|
|
1150
1204
|
root_item = dm.CreateDevice(0, scanner.id)
|
|
1151
1205
|
except Exception as exc:
|
|
1152
|
-
|
|
1206
|
+
hr = getattr(exc, "hresult", None)
|
|
1207
|
+
if hr in _WIA_BUSY_ERRORS:
|
|
1153
1208
|
raise ScannerBusyError() from exc
|
|
1209
|
+
if hr in _WIA_UNAVAILABLE_ERRORS:
|
|
1210
|
+
raise ScannerUnavailableError(
|
|
1211
|
+
f"Scanner {scanner.id!r} is unavailable — it may be "
|
|
1212
|
+
"offline, asleep, or disconnected. Check that it is powered "
|
|
1213
|
+
"on and reachable, then try again."
|
|
1214
|
+
) from exc
|
|
1154
1215
|
raise ScanError(f"Failed to open scanner {scanner.id!r}: {exc}") from exc
|
|
1155
1216
|
|
|
1156
1217
|
# Read device-level properties from root item
|
|
@@ -1165,46 +1226,31 @@ class WiaBackend:
|
|
|
1165
1226
|
if not source_types:
|
|
1166
1227
|
source_types = [ScanSource.FLATBED]
|
|
1167
1228
|
|
|
1168
|
-
#
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
enum_items = root_item.EnumChildItems(None)
|
|
1172
|
-
child, fetched = enum_items.Next(1)
|
|
1173
|
-
if fetched:
|
|
1174
|
-
child_item = child
|
|
1175
|
-
except Exception:
|
|
1176
|
-
pass
|
|
1229
|
+
# Map each source to its dedicated child item (Flatbed/Feeder),
|
|
1230
|
+
# falling back to the first item for single-item scanners.
|
|
1231
|
+
child_map, default_child = _enum_scan_items(root_item)
|
|
1177
1232
|
|
|
1178
1233
|
source_infos: list[SourceInfo] = []
|
|
1179
|
-
if
|
|
1234
|
+
if default_child is not None:
|
|
1180
1235
|
try:
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
# Read per-source resolutions and color modes.
|
|
1236
|
+
# Read each source's resolutions/color modes from its own
|
|
1237
|
+
# child item (the feeder may differ from the flatbed).
|
|
1185
1238
|
for source in source_types:
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
select_val = (
|
|
1189
|
-
_FEEDER if source == ScanSource.FEEDER else _FLATBED
|
|
1190
|
-
)
|
|
1191
|
-
_write_prop(
|
|
1192
|
-
root_storage,
|
|
1193
|
-
_WIA_DPS_DOCUMENT_HANDLING_SELECT,
|
|
1194
|
-
select_val,
|
|
1195
|
-
)
|
|
1196
|
-
except Exception:
|
|
1197
|
-
pass
|
|
1239
|
+
src_item = child_map.get(source, default_child)
|
|
1240
|
+
item_storage = src_item.QueryInterface(IWiaPropertyStorage)
|
|
1198
1241
|
source_infos.append(
|
|
1199
1242
|
SourceInfo(
|
|
1200
1243
|
type=source,
|
|
1201
1244
|
resolutions=_read_wia_resolutions(item_storage),
|
|
1202
1245
|
color_modes=_read_wia_color_modes(item_storage),
|
|
1203
|
-
max_scan_area=
|
|
1246
|
+
max_scan_area=_read_wia_max_scan_area(
|
|
1247
|
+
root_storage, item_storage
|
|
1248
|
+
),
|
|
1204
1249
|
)
|
|
1205
1250
|
)
|
|
1206
1251
|
|
|
1207
|
-
|
|
1252
|
+
default_storage = default_child.QueryInterface(IWiaPropertyStorage)
|
|
1253
|
+
scanner._defaults = _read_wia_defaults(default_storage, source_types)
|
|
1208
1254
|
except Exception:
|
|
1209
1255
|
scanner._defaults = None
|
|
1210
1256
|
else:
|
|
@@ -1222,7 +1268,7 @@ class WiaBackend:
|
|
|
1222
1268
|
scanner._defaults = None
|
|
1223
1269
|
|
|
1224
1270
|
scanner._sources = source_infos
|
|
1225
|
-
self._handles[scanner.id] = (root_item,
|
|
1271
|
+
self._handles[scanner.id] = (root_item, child_map, default_child)
|
|
1226
1272
|
|
|
1227
1273
|
def _close_scanner_impl(self, scanner: Scanner) -> None:
|
|
1228
1274
|
self._handles.pop(scanner.id, None)
|
|
@@ -1234,7 +1280,11 @@ class WiaBackend:
|
|
|
1234
1280
|
if items is None:
|
|
1235
1281
|
raise ScanError("Scanner is not open")
|
|
1236
1282
|
|
|
1237
|
-
root_item,
|
|
1283
|
+
root_item, child_map, default_child = items
|
|
1284
|
+
source = options.source or ScanSource.FLATBED
|
|
1285
|
+
# Scan from the source's own child item — using the flatbed item for
|
|
1286
|
+
# a feeder scan makes some drivers scan blanks from an empty feeder.
|
|
1287
|
+
child_item = child_map.get(source, default_child)
|
|
1238
1288
|
if child_item is None:
|
|
1239
1289
|
raise ScanError("Scanner has no scan items")
|
|
1240
1290
|
|
|
@@ -1296,34 +1346,47 @@ class WiaBackend:
|
|
|
1296
1346
|
|
|
1297
1347
|
while True:
|
|
1298
1348
|
callback = _TransferCallback(options.progress, scanner._abort_event)
|
|
1349
|
+
end_of_feeder = False
|
|
1299
1350
|
try:
|
|
1300
1351
|
transfer.Download(0, callback)
|
|
1301
1352
|
except Exception as exc:
|
|
1353
|
+
# Classify by HRESULT, never by message text.
|
|
1302
1354
|
hr = getattr(exc, "hresult", None)
|
|
1303
|
-
|
|
1304
|
-
if callback._aborted:
|
|
1355
|
+
if callback._aborted or hr in _WIA_CANCEL_ERRORS:
|
|
1305
1356
|
raise ScanAborted("Scan aborted") from exc
|
|
1306
|
-
if
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1357
|
+
if hr in _WIA_BUSY_ERRORS:
|
|
1358
|
+
raise ScannerBusyError() from exc
|
|
1359
|
+
if hr in _WIA_UNAVAILABLE_ERRORS:
|
|
1360
|
+
raise ScannerUnavailableError() from exc
|
|
1361
|
+
if hr == _WIA_ERROR_PAPER_EMPTY:
|
|
1362
|
+
# Feeder ran dry: empty from the start if nothing was
|
|
1363
|
+
# scanned, otherwise the normal end of a feeder run.
|
|
1311
1364
|
if is_feeder and not all_pages and not callback.pages:
|
|
1312
1365
|
raise FeederEmptyError("No documents in feeder") from exc
|
|
1313
|
-
|
|
1314
|
-
elif
|
|
1315
|
-
|
|
1316
|
-
elif "cancel" in msg_text or "abort" in msg_text:
|
|
1317
|
-
raise ScanAborted(f"Scan cancelled by device: {exc}") from exc
|
|
1318
|
-
elif not callback.pages and not all_pages:
|
|
1366
|
+
end_of_feeder = True
|
|
1367
|
+
elif not all_pages and not callback.pages:
|
|
1368
|
+
# Unrecognised failure with nothing scanned — a real error.
|
|
1319
1369
|
raise ScanError(f"Scan failed: {exc}") from exc
|
|
1370
|
+
else:
|
|
1371
|
+
# Unrecognised error after pages on a feeder — stop the
|
|
1372
|
+
# run rather than risk looping on a repeating failure.
|
|
1373
|
+
end_of_feeder = True
|
|
1320
1374
|
|
|
1321
1375
|
all_pages.extend(callback.pages)
|
|
1322
1376
|
|
|
1323
|
-
if not is_feeder:
|
|
1377
|
+
if not is_feeder or end_of_feeder:
|
|
1378
|
+
break
|
|
1379
|
+
if not callback.pages:
|
|
1380
|
+
# Feeder Download returned successfully but produced no
|
|
1381
|
+
# page and didn't raise WIA_ERROR_PAPER_EMPTY — some
|
|
1382
|
+
# scanners (e.g. Epson WSD) signal an empty feeder this
|
|
1383
|
+
# way. Stop instead of calling Download again, which
|
|
1384
|
+
# would re-run the scanner on an empty feeder forever.
|
|
1324
1385
|
break
|
|
1325
1386
|
|
|
1326
1387
|
if not all_pages:
|
|
1388
|
+
if is_feeder:
|
|
1389
|
+
raise FeederEmptyError("No documents in feeder")
|
|
1327
1390
|
raise ScanError("No pages were scanned")
|
|
1328
1391
|
|
|
1329
1392
|
check_progress(options.progress, 100)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|