-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscanline_wrapper.py
More file actions
314 lines (239 loc) · 9.32 KB
/
scanline_wrapper.py
File metadata and controls
314 lines (239 loc) · 9.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import os
import subprocess
import tempfile
import shutil
import logging
from enum import Enum
from pathlib import Path
logger = logging.getLogger("scanline_wrapper")
class PageSize(Enum):
"""Page size."""
A4 = "-a4"
LEGAL = "-legal"
LETTER = "-letter"
class FileFormat(Enum):
"""Output file format."""
AUTO = -1
PDF = None
TIFF = "-tiff"
JPEG = "-jpeg"
class Color(Enum):
"""Color or monochrome output."""
COLOR = None
MONOCHROME = "-mono"
class ScanlineException(Exception):
"""Base class for Scanline exceptions."""
class ScanlineUnknownError(ScanlineException):
pass
class ScanlineExecutableNotFound(ScanlineException):
pass
class ScanlineScannerNotFound(ScanlineException):
pass
class ScanlineInvalidPageSize(ScanlineException):
pass
class ScanlineInvalidFileFormat(ScanlineException):
pass
class ScanlineInvalidColor(ScanlineException):
pass
_FILE_EXT_TO_FORMAT = {
".pdf": FileFormat.PDF,
".tif": FileFormat.TIFF,
".tiff": FileFormat.TIFF,
".jpg": FileFormat.JPEG,
".jpeg": FileFormat.JPEG,
}
_FORMAT_TO_FILE_EXT = {
FileFormat.PDF: ".pdf",
FileFormat.TIFF: ".tif",
FileFormat.JPEG: ".jpg",
}
def _get_scanline_cmd():
"""Get the scanline command name or path.
This can be overided by the ``SCANLINE_CMD`` environment variable.
:rtype: str
"""
return os.environ.get("SCANLINE_CMD", "scanline")
def _is_scanline_available():
"""Checks if the scanline command is available.
:rtype: bool
"""
cmd = _get_scanline_cmd()
if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
return True
if "PATH" in os.environ:
for path in os.environ["PATH"].split(":"):
cmd_path = os.path.join(path, cmd)
if os.path.isfile(cmd_path) and os.access(cmd_path, os.X_OK):
return True
return False
def list_scanners(browsesecs=1, verbose=False):
"""Get a list of available scanners.
Example:
>>> import scanline_wrapper
>>> scanline_wrapper.list_scanners()
... ['HP Color LaserJet MFP M281fdw (035F4A)', 'My other scanner']
:param int browsesecs: Specify how long to wait when searching for scanners
(in seconds, default: ``1``).
:param bool verbose: Increase verbosity of scanline logs (default: ``False``).
:raise ScanlineExecutableNotFound: if the scanline app is not installed.
:raise subprocess.CalledProcessError: if something goes wrong while running
the scanline command.
:rtype: list(str)
:returns: the available scanners.
"""
if verbose:
logging.basicConfig(level=logging.INFO)
if not _is_scanline_available():
raise ScanlineExecutableNotFound(
"The scanline command was not found. Is scanline installed?"
)
command = [_get_scanline_cmd()]
command += ["-list"]
command += ["-browsesecs", str(browsesecs)]
if verbose:
command += ["-verbose"]
logger.info("Running command: %s" % " ".join(command))
proc = subprocess.run(command, check=True, capture_output=True)
logger.info(proc.stdout.decode("UTF-8", errors="ignore"))
scanners = []
for line in proc.stdout.decode("UTF-8", errors="ignore").split("\n"):
if line.startswith("* "):
scanners.append(line[2:])
return scanners
def scan_flatbed(
output_path,
scanner=None,
is_scanner_exact_name=False,
page_size=PageSize.A4,
file_format=FileFormat.AUTO,
color=Color.COLOR,
resolution=150,
browsesecs=1,
verbose=False,
):
"""Scans a document using the flatbed unit of the scanner.
Simple example:
>>> import scanline_wrapper
>>> scanline_wrapper.scan_flatbed("./out.tiff")
More complete example:
>>> import scanline_wrapper
>>> scanline_wrapper.scan_flatbed(
>>> "./out.jpg",
>>> scanner="HP Color LaserJet MFP M281fdw (035F4A)",
>>> page_size=scanline_wrapper.PageSize.LETTER, # A4, LEGAL or LETTER
>>> file_format=scanline_wrapper.FileFormat.JPEG, # AUTO, PDF, TIFF or JPEG
>>> color=scanline_wrapper.Color.COLOR, # COLOR or MONOCHROME
>>> resolution=150, # DPI
>>> )
:param str,pathlib.Path output_path: The output file path.
:param str scanner: The name of the scanner to use. If not provided, the
first available scanner will be used (default: ``None``).
:param bool is_scanner_exact_name: If set to ``True``, scanline will try to
fuzzy-match the scanner name (default: ``False``).
:param PageSize page_size: The size of the page to scan (default.
``PageSize.A4``).
:param FileFormat file_format: The output file format. If set to
``FileFormat.AUTO`` the format will be infered from the file extension.
A ``ValueError`` will be raised if the file extension does not match a
supported format. (default: ``FileFormat.AUTO``).
:param Color color: Select color or monochrome scan (default:
``Color.COLOR``).
:param int resolution: Specify minimum resolution at which to scan (in dpi,
default: ``150``).
:param int browsesecs: Specify how long to wait when searching for scanners
(in seconds, default: ``1``).
:param bool verbose: Increase verbosity of scanline logs (default: ``False``).
:raise ScanlineScannerNotFound: if the scanner requested in ``scanner``
cannot be found or if no scanner are found.
:raise ScanlineInvalidPageSize: if the given page size is not one from the
:py:class:`~PageSize` enum.
:raise ScanlineInvalidFileFormat: if the given file_format is not one from the
:py:class:`~FileFormat` or if the file extension is not recognized when
file format is set to :py:attr:`FileFormat.AUTO`.
:raise ScanlineInvalidColor: if the given page color is not one from the
:py:class:`~Color` enum.
:raise ScanlineExecutableNotFound: if the scanline app is not installed.
:raise ScanlineUnknownError: if scanline has not generated the expected
output file without returning a specific error.
:raise subprocess.CalledProcessError: if something goes wrong while running
the scanline command.
:rtype: None
"""
if not _is_scanline_available():
raise ScanlineExecutableNotFound(
"The scanline command was not found. Is scanline installed?"
)
if verbose:
logging.basicConfig(level=logging.INFO)
# Normalize path
output_path = Path(output_path).absolute()
command = [_get_scanline_cmd()]
command += ["-flatbed"]
# Scanner selection
if scanner:
command += ["-scanner", scanner]
if is_scanner_exact_name:
command += ["-exactname"]
# Page Size
if type(page_size) is PageSize:
command += [page_size.value]
elif page_size in [item.value for item in PageSize]:
command += [page_size]
else:
raise ScanlineInvalidPageSize("Invalid page size: %s." % str(page_size))
# File Format
if type(file_format) is not FileFormat:
if file_format in [item.value for item in FileFormat]:
file_format = FileFormat(file_format)
else:
raise ScanlineInvalidFileFormat(
"Invalid file format: %s." % str(file_format)
)
if file_format == FileFormat.AUTO:
if output_path.suffix.lower() in _FILE_EXT_TO_FORMAT:
file_format = _FILE_EXT_TO_FORMAT[output_path.suffix.lower()]
else:
raise ScanlineInvalidFileFormat(
"Auto file format: unsupported file extension: %s"
% str(output_path.suffix)
)
if file_format.value: # PDF == None (default behaviour, no argument)
command += [file_format.value]
# Color
if type(color) is Color:
if color.value:
command += [color.value]
elif color in [item.value for item in Color]:
if color:
command += [color]
else:
raise ScanlineInvalidColor("Invalid color: %s." % str(color))
# Resolution
command += ["-resolution", str(resolution)]
# Browse wait time
command += ["-browsesecs", str(browsesecs)]
# Verbose
if verbose:
command += ["-verbose"]
with tempfile.TemporaryDirectory(prefix="scanline_wrapper_") as tmp_dir:
# Output file
tmp_output_path = (Path(tmp_dir) / "scan").with_suffix(
_FORMAT_TO_FILE_EXT[file_format]
)
command += ["-dir", tmp_output_path.parent.as_posix()]
command += ["-name", tmp_output_path.with_suffix("").name]
# Call scanline
logger.info("Running command: %s" % " ".join(command))
proc = subprocess.run(command, check=True, capture_output=True)
logger.info(proc.stdout.decode("UTF-8", errors="ignore"))
for line in proc.stdout.decode("UTF-8", errors="ignore").split("\n"):
if line == "No scanner was found.":
raise ScanlineScannerNotFound("No scanner was found.")
# Check output file was created
if not tmp_output_path.exists():
raise ScanlineUnknownError(
"Expected output file was not generated by scanline."
)
# Move output file to its final destination
if output_path != tmp_output_path:
shutil.move(tmp_output_path, output_path)