-
Notifications
You must be signed in to change notification settings - Fork 0
/
sanesane.py
executable file
·239 lines (195 loc) · 7.6 KB
/
sanesane.py
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
#!/home/resi/opt/virtualenvs/sane/bin/python
import sys
import os
import logging
import tempfile
import subprocess
import pyinsane.abstract as pyinsane
import PyPDF2 as pypdf
import PIL.Image
DPI = 300
TARGET_DPI = 75
logging.basicConfig(level=logging.INFO)
log = logging.getLogger()
def spawnDaemon(func):
# do the UNIX double-fork magic, see Stevens' "Advanced
# Programming in the UNIX Environment" for details (ISBN 0201563177)
try:
pid = os.fork()
if pid > 0:
# parent process, return and keep running
return
except OSError as e:
log.exception("fork #1 failed: %d (%s)" % (e.errno, e.strerror))
sys.exit(1)
os.setsid()
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as e:
log.exception("fork #2 failed: %d (%s)" % (e.errno, e.strerror))
sys.exit(1)
# do stuff
func()
# all done
os._exit(os.EX_OK)
def open_file(fn):
subprocess.Popen(["gio", "open", fn])
def decode(data):
if isinstance(data, (list, tuple)):
return [decode(d) for d in data]
if isinstance(data, bytes):
return data.decode()
return str(data)
def find_in_str_ic(ls, pat):
try:
return [s for s in ls if pat.lower() in s.lower()][0]
except IndexError:
return None
def find_devices():
devices = pyinsane.get_devices()
return devices
def open_device(device):
if device.startswith("#"):
dev = find_devices()[int(device[1:])]
else:
dev = pyinsane.Scanner(device)
return dev
def find_sources(dev):
return decode(dev.options["source"].constraint)
def find_resolutions(dev):
return decode(dev.options["resolution"].constraint)
def wait_for_key(hint="Press enter to continue\n"):
return input(hint)
def scan_n_pages(dev, num_pages=0, duplex=False, resolution=DPI):
num_pages = max(num_pages, 0)
sources = find_sources(dev)
# Note:
# It is _very_ important to set the "source" option before (any)
# "resolution" option. Setting the source seems to reset the resolution to
# the device default value.
# Taken from https://bugs.launchpad.net/simple-scan/+bug/891586/comments/25
def update_options(source=None):
if source:
dev.options["source"].value = source
dev.options["color"] = "Color"
if "adf-mode" in dev.options:
dev.options["adf-mode"].value = "Duplex" if duplex else "Simplex"
#dev.options["scan-area"].value = "A4"
dev.options["resolution"].value = resolution
return dev.options["resolution"].value
# update once to determine the next matching resolution
resolution = update_options()
log.debug("using resolution %s dpi" % (resolution,))
try:
log.debug("trying ADF")
is_adf = True
update_options(find_in_str_ic(sources, "feed"))
scan_session = pyinsane.ScanSession(pyinsane.MultipleScan(dev))
except StopIteration:
log.debug("ADF empty")
is_adf = False
update_options(find_in_str_ic(sources, "flat"))
scan_session = pyinsane.ScanSession(pyinsane.MultipleScan(dev))
if not num_pages:
num_pages = 1
page_count = 0
while True:
try:
try:
scan_session.scan.read()
except EOFError:
page_count += 1
if is_adf and duplex and not (page_count % 2):
scan_session.images[-1] = scan_session.images[-1].rotate(180)
if page_count == num_pages:
raise StopIteration
if not is_adf:
wait_for_key()
except StopIteration:
break
log.debug("read %s pages" % (page_count,))
return scan_session.images, resolution
def scale_image(im, current_resolution, target_resolution=TARGET_DPI):
f = target_resolution / current_resolution
w, h = im.size
return im.resize((int(w * f), int(h * f)), PIL.Image.LANCZOS)
def make_pdf(fn, images, resolution=DPI):
if not images:
return
outfile = open(fn, "wb")
if len(images) == 1:
images[0].save(outfile, "PDF", resolution=resolution)
else:
pdf_out = pypdf.PdfFileWriter()
for image in images:
tfo = tempfile.TemporaryFile()
image.save(tfo, "PDF", resolution=resolution)
pdf_in = pypdf.PdfFileReader(tfo)
for i in range(pdf_in.numPages):
pdf_out.addPage(pdf_in.getPage(i))
pdf_out.write(outfile)
def argparse():
import argparse
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-L", "--list-devices", action="store_true",
help="find availabe devices")
parser.add_argument("-n", "--num-pages", default=1, type=int,
help="number of pages to scan (automatic if 0)")
parser.add_argument("-D", "--device", default="#0",
help="device number (#<num>) or identification to use")
parser.add_argument("-S", "--show-sources", action="store_true",
help="show available sources for the current device")
parser.add_argument("-s", "--source", default="auto",
help="use specified source (auto, any entry from --show-sources)")
parser.add_argument("-d", "--duplex", action="store_true",
help="use duplex mode")
parser.add_argument("--dpi", default=DPI, type=int,
help="scanner resolution")
parser.add_argument("--target-dpi", default=TARGET_DPI, type=int,
help="output resolution")
parser.add_argument("-r", "--rotate", default=0, type=int,
help="rotate image counter-clockwise (degrees)")
parser.add_argument("-R", "--show-resolutions", action="store_true",
help="show available resolutions for the current device")
parser.add_argument("-v", "--view", action="store_true",
help="show document after scanning (using gio open)")
parser.add_argument("-f", "--force", action="store_true",
help="force overwriting of existing files")
parser.add_argument("outfile", metavar="OUTFILE", nargs="?", default="scan.pdf",
help="name of output file")
return parser.parse_args()
def cli_main(args):
if args.list_devices:
devs = find_devices()
print("Found %s devices:" % (len(devs),))
for i, dev in enumerate(devs):
print("#%s: %s" % (i, dev.name))
return
if os.path.exists(args.outfile) and not args.force:
raise Exception("File exists: %s\nUse --force to overwrite." % args.outfile)
dev = open_device(args.device)
log.debug("Using device '%s'" % (dev.name,))
if args.show_sources:
sources = find_sources(dev)
print("Available sources:", ", ".join(sources))
return
if args.show_resolutions:
resolutions = find_resolutions(dev)
print("Available resolutions:", ", ".join(resolutions))
return
images, resolution = scan_n_pages(dev, args.num_pages, args.duplex, args.dpi)
if args.rotate:
images = [im.rotate(args.rotate, expand=True) for im in images]
images = [scale_image(im, resolution, args.target_dpi) for im in images]
make_pdf(args.outfile, images, args.target_dpi)
if args.view:
spawnDaemon(lambda: open_file(args.outfile))
return args.outfile
def main():
cli_main(argparse())
if __name__ == "__main__":
main()