1 |
#!/usr/bin/env python |
2 |
# |
3 |
# Author: Sascha Silbe <sascha-pgp@silbe.org> |
4 |
# |
5 |
# This program is free software; you can redistribute it and/or modify |
6 |
# it under the terms of the GNU General Public License version 3 |
7 |
# as published by the Free Software Foundation. |
8 |
# |
9 |
# This program is distributed in the hope that it will be useful, |
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 |
# GNU General Public License for more details. |
13 |
# |
14 |
# You should have received a copy of the GNU General Public License |
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 |
"""Scan: Import images using a document scanner. |
17 |
""" |
18 |
from __future__ import with_statement |
19 |
|
20 |
from gettext import lgettext as _ |
21 |
from gettext import lngettext as ngettext |
22 |
import logging |
23 |
import math |
24 |
import os |
25 |
import sys |
26 |
import tempfile |
27 |
import time |
28 |
|
29 |
import gobject |
30 |
import gtk |
31 |
|
32 |
import reportlab.pdfgen.canvas |
33 |
from reportlab.lib import units |
34 |
import sane |
35 |
import threading |
36 |
|
37 |
from sugar.activity.widgets import ActivityToolbarButton, StopButton |
38 |
from sugar.graphics.toggletoolbutton import ToggleToolButton |
39 |
from sugar.activity import activity |
40 |
from sugar.datastore import datastore |
41 |
from sugar.graphics.toolbutton import ToolButton |
42 |
from sugar.graphics.combobox import ComboBox |
43 |
from sugar.graphics.toolbarbox import ToolbarButton, ToolbarBox |
44 |
from sugar.graphics import style |
45 |
from sugar.logger import trace |
46 |
|
47 |
|
48 |
class SettingsToolbar(gtk.Toolbar): |
49 |
|
50 |
@trace() |
51 |
def __init__(self): |
52 |
gtk.Toolbar.__init__(self) |
53 |
self._scanner = None |
54 |
self.props.show_arrow = True |
55 |
|
56 |
def set_scanner(self, scanner): |
57 |
self._scanner = scanner |
58 |
self._clear() |
59 |
if self._scanner: |
60 |
self._add_options() |
61 |
|
62 |
def _clear(self): |
63 |
for widget in list(self): |
64 |
self.remove(widget) |
65 |
|
66 |
@trace() |
67 |
def _get_groups(self): |
68 |
group = [] |
69 |
groups = [(None, group)] |
70 |
options = self._scanner.get_options() |
71 |
for idx, name, title, dsc_, _type, unit_, siz_, cap_, cons_ in options: |
72 |
if _type == sane.TYPE_GROUP: |
73 |
group = [] |
74 |
groups.append((title, group)) |
75 |
continue |
76 |
|
77 |
if not name and idx != 0: |
78 |
logging.warning('ignoring unnamed option (index %d)', idx) |
79 |
continue |
80 |
|
81 |
group.append(name) |
82 |
|
83 |
return groups |
84 |
|
85 |
def _add_options(self): |
86 |
groups = self._get_groups() |
87 |
name_map = dict([(self._scanner[py_name].name, py_name) |
88 |
for py_name in self._scanner.optlist]) |
89 |
|
90 |
first_group = True |
91 |
for group_name, group in groups: |
92 |
if not group: |
93 |
continue |
94 |
|
95 |
if first_group: |
96 |
first_group = False |
97 |
else: |
98 |
self.insert(gtk.SeparatorToolItem(), -1) |
99 |
|
100 |
# TODO: handle SANE_CAP_ADVANCED and maybe other capabilities |
101 |
# TODO: handle area by paper size presets + single fields in |
102 |
# advanced options |
103 |
|
104 |
for option_name in group: |
105 |
py_name = name_map[option_name] |
106 |
option = self._scanner[py_name] |
107 |
if not option.is_active(): |
108 |
# FIXME: check SANE docs what inactive is used for |
109 |
# I have a feeling we might need to cope with this |
110 |
# changing after changing other settings like the mode |
111 |
# (color/gray/...) |
112 |
logging.warning('skipping inactive option %r', option_name) |
113 |
continue |
114 |
|
115 |
value = getattr(self._scanner, py_name) |
116 |
if (option.type == sane.TYPE_BOOL): |
117 |
widget = self._add_bool(option, value) |
118 |
elif (option.type == sane.TYPE_INT): |
119 |
widget = self._add_integer(option, value) |
120 |
elif (option.type == sane.TYPE_FIXED): |
121 |
widget = self._add_fixed_point(option, value) |
122 |
elif (option.type == sane.TYPE_STRING): |
123 |
widget = self._add_string(option, value) |
124 |
elif (option.type == sane.TYPE_BUTTON): |
125 |
widget = self._add_action(option, value) |
126 |
|
127 |
if not widget: |
128 |
continue |
129 |
|
130 |
widget.set_sensitive(option.is_settable()) |
131 |
|
132 |
# FIXME: take translation from SANE |
133 |
label = gtk.Label(_(option.title)) |
134 |
hbox = gtk.HBox(False, style.DEFAULT_SPACING) |
135 |
hbox.pack_start(label, False) |
136 |
hbox.pack_start(widget) |
137 |
label.show() |
138 |
|
139 |
tool_item = gtk.ToolItem() |
140 |
tool_item.add(hbox) |
141 |
# FIXME: take translation from SANE |
142 |
tool_item.set_tooltip_text(_(option.desc)) |
143 |
# TODO: represent unit |
144 |
self.insert(tool_item, -1) |
145 |
|
146 |
self.show_all() |
147 |
|
148 |
def _add_bool(self, option, value): |
149 |
button = gtk.CheckButton() |
150 |
button.set_active(value) |
151 |
button.connect('toggled', lambda widget, |
152 |
option=option: self._toggle_cb(widget, option)) |
153 |
return button |
154 |
|
155 |
def _add_integer(self, option, value): |
156 |
if isinstance(option.constraint, tuple): |
157 |
return self._add_spinbutton(option, value) |
158 |
elif isinstance(option.constraint, list): |
159 |
# TODO |
160 |
return None |
161 |
return self._add_spinbutton(option, value) |
162 |
|
163 |
def _add_fixed_point(self, option, value): |
164 |
if isinstance(option.constraint, tuple): |
165 |
return self._add_spinbutton(option, value) |
166 |
elif isinstance(option.constraint, list): |
167 |
# TODO |
168 |
return None |
169 |
else: |
170 |
# TODO |
171 |
return None |
172 |
|
173 |
@trace() |
174 |
def _add_string(self, option, value): |
175 |
if option.constraint: |
176 |
return self._add_combo(option, value) |
177 |
|
178 |
return self._add_entry(option, value) |
179 |
|
180 |
def _add_action(self, option, value): |
181 |
# TODO |
182 |
return None |
183 |
|
184 |
def _add_spinbutton(self, option, value): |
185 |
if isinstance(option.constraint, tuple): |
186 |
lower, upper, step = option.constraint |
187 |
else: |
188 |
lower, upper, step = 0, sys.maxint, 1 |
189 |
|
190 |
if not step: |
191 |
# no quantization set => guess it |
192 |
step = 1 |
193 |
|
194 |
spin_adj = gtk.Adjustment(value, lower, upper, step, step*10, 0) |
195 |
spin = gtk.SpinButton(spin_adj, 0, 0) |
196 |
spin.props.snap_to_ticks = True |
197 |
spin.set_digits(self._calc_digits(step)) |
198 |
|
199 |
if isinstance(value, float): |
200 |
spin.connect('value-changed', lambda widget, option=option: |
201 |
self._spin_float_changed_cb(widget, option)) |
202 |
else: |
203 |
spin.connect('value-changed', lambda widget, option=option: |
204 |
self._spin_int_changed_cb(widget, option)) |
205 |
|
206 |
spin.set_numeric(True) |
207 |
spin.show() |
208 |
|
209 |
return spin |
210 |
|
211 |
def _calc_digits(self, step): |
212 |
digits = 0 |
213 |
while math.modf(step)[0] > .001: |
214 |
digits += 1 |
215 |
step *= 10 |
216 |
|
217 |
return digits |
218 |
|
219 |
def _add_entry(self, option, value): |
220 |
# untested |
221 |
entry = gtk.Entry() |
222 |
entry.set_text(value) |
223 |
entry.connect('focus-out-event', lambda widget, option=option: |
224 |
self._entry_changed_cb(widget, option)) |
225 |
entry.connect('activated', lambda widget, option=option: |
226 |
self._entry_changed_cb(widget, option)) |
227 |
return entry |
228 |
|
229 |
@trace() |
230 |
def _add_combo(self, option, value): |
231 |
box = ComboBox() |
232 |
for combo_value in option.constraint: |
233 |
# FIXME: take translation from SANE |
234 |
box.append_item(combo_value, _(combo_value)) |
235 |
|
236 |
new_idx = option.constraint.index(value) |
237 |
box.set_active(new_idx) |
238 |
box.connect('changed', lambda widget, option=option: |
239 |
self._combo_changed_cb(widget, option)) |
240 |
return box |
241 |
|
242 |
@trace() |
243 |
def _toggle_cb(self, button, option): |
244 |
new_value = button.get_active() |
245 |
setattr(self._scanner, option.py_name, new_value) |
246 |
|
247 |
@trace() |
248 |
def _entry_changed_cb(self, entry, option): |
249 |
setattr(self._scanner, option.py_name, entry.get_text()) |
250 |
|
251 |
@trace() |
252 |
def _combo_changed_cb(self, combo, option): |
253 |
setattr(self._scanner, option.py_name, combo.get_value()) |
254 |
|
255 |
@trace() |
256 |
def _spin_float_changed_cb(self, spin, option): |
257 |
setattr(self._scanner, option.py_name, spin.get_value()) |
258 |
|
259 |
@trace() |
260 |
def _spin_int_changed_cb(self, spin, option): |
261 |
setattr(self._scanner, option.py_name, int(spin.get_value())) |
262 |
|
263 |
|
264 |
class ScanThread(threading.Thread): |
265 |
|
266 |
def __init__(self, devs, temp_dir, callback=None): |
267 |
threading.Thread.__init__(self) |
268 |
self._devs = devs |
269 |
self._temp_dir = temp_dir |
270 |
self._callback = callback |
271 |
self._cond = threading.Condition() |
272 |
self._action = None |
273 |
self._images = [] |
274 |
self._error = None |
275 |
self._ready = False |
276 |
self._dpi = None |
277 |
|
278 |
def run(self): |
279 |
while True: |
280 |
with self._cond: |
281 |
while not self._action: |
282 |
logging.debug('waiting') |
283 |
self._cond.wait() |
284 |
logging.debug('woken') |
285 |
|
286 |
action = self._action |
287 |
self._action = None |
288 |
|
289 |
if action == 'quit': |
290 |
return |
291 |
|
292 |
if action == 'start': |
293 |
self._scan() |
294 |
|
295 |
def stop_thread(self): |
296 |
with self._cond: |
297 |
self._action = 'quit' |
298 |
self._cond.notify() |
299 |
|
300 |
@trace() |
301 |
def start_scan(self): |
302 |
with self._cond: |
303 |
logging.debug('start_scan(): got lock') |
304 |
self._action = 'start' |
305 |
self._cond.notify() |
306 |
logging.debug('start_scan(): notified') |
307 |
|
308 |
logging.debug('start_scan(): lock released') |
309 |
|
310 |
@trace() |
311 |
def stop_scan(self): |
312 |
with self._cond: |
313 |
logging.debug('stop_scan(): got lock') |
314 |
self._action = 'stop' |
315 |
self._cond.notify() |
316 |
logging.debug('stop_scan(): notified') |
317 |
|
318 |
logging.debug('stop_scan(): lock released') |
319 |
|
320 |
def is_ready(self): |
321 |
with self._cond: |
322 |
return self._ready |
323 |
|
324 |
def get_result(self): |
325 |
with self._cond: |
326 |
if self._error: |
327 |
raise self._error |
328 |
|
329 |
images = self._images |
330 |
self._images = [] |
331 |
return images |
332 |
|
333 |
@trace() |
334 |
def _scan(self): |
335 |
self._images = None |
336 |
self._error = None |
337 |
|
338 |
source = getattr(self._devs[0], 'source', 'Auto') |
339 |
if getattr(self._devs[0], 'batch_scan', False): |
340 |
scan_multi = source in ['Auto', 'ADF'] |
341 |
else: |
342 |
scan_multi = source == 'ADF' |
343 |
|
344 |
try: |
345 |
if scan_multi: |
346 |
self._scan_multi() |
347 |
else: |
348 |
logging.debug('starting single scan') |
349 |
self._dpi = self._devs[0].resolution |
350 |
self._images = [self._process_image(self._devs[0].scan())] |
351 |
|
352 |
except sane.error, exc: |
353 |
# Ouch. sane.error doesn't derive from Exception :-/ |
354 |
self._error = ValueError('SANE error: %s' % (exc, )) |
355 |
|
356 |
except Exception, exc: |
357 |
self._error = exc |
358 |
|
359 |
with self._cond: |
360 |
self._ready = True |
361 |
|
362 |
try: |
363 |
self._callback(self._images, self._error) |
364 |
|
365 |
except: |
366 |
logging.exception('_scan(): Uncaught exception in callback') |
367 |
|
368 |
@trace() |
369 |
def _scan_multi(self): |
370 |
logging.debug('_scan: calling multi_scan()') |
371 |
scan_iter = self._devs[0].multi_scan() |
372 |
|
373 |
logging.debug('_scan: acquiring images') |
374 |
images = [] |
375 |
try: |
376 |
self._dpi = self._devs[0].resolution |
377 |
for image in scan_iter: |
378 |
images.append(self._process_image(image)) |
379 |
if self._check_action(['stop', 'quit']): |
380 |
break |
381 |
|
382 |
self._dpi = self._devs[0].resolution |
383 |
|
384 |
self._images = images |
385 |
|
386 |
finally: |
387 |
logging.debug('_scan(): closing iterator') |
388 |
# make sure the scan is "cancelled" _now_ |
389 |
del scan_iter |
390 |
logging.debug('_scan(): iterator closed') |
391 |
|
392 |
@trace() |
393 |
def _check_action(self, actions): |
394 |
with self._cond: |
395 |
return self._action in actions |
396 |
|
397 |
def _process_image(self, image): |
398 |
image_file = tempfile.NamedTemporaryFile(dir=self._temp_dir, |
399 |
suffix='.png') |
400 |
image.save(image_file.name) |
401 |
image_file.flush() |
402 |
info = { |
403 |
'file': image_file, |
404 |
'width_pixel': image.size[0], |
405 |
'height_pixel': image.size[1], |
406 |
'dpi': self._dpi, |
407 |
} |
408 |
del image |
409 |
return info |
410 |
|
411 |
|
412 |
class ScanActivity(activity.Activity): |
413 |
|
414 |
_STATUS_MSGS = { |
415 |
'no-dev': _('No scanner found'), |
416 |
'ready': _('Ready'), |
417 |
'stopping': _('Stopping...'), |
418 |
'scanning': _('Scanning...'), |
419 |
'error': _('Error: %s'), |
420 |
} |
421 |
|
422 |
def __init__(self, handle): |
423 |
activity.Activity.__init__(self, handle) |
424 |
self.max_participants = 1 |
425 |
self._scanner_name = None |
426 |
self._scanners = [] |
427 |
self._scanner_options = {'source': 'ADF'} |
428 |
self._image_infos = [] |
429 |
self._status = 'init' |
430 |
self._msg_box_buf = None |
431 |
self._status_entry = None |
432 |
self._scan_button = None |
433 |
self._settings_toolbar = None |
434 |
temp_dir = os.path.join(self.get_activity_root(), 'tmp') |
435 |
self._scan_thread = ScanThread(self._scanners, temp_dir, |
436 |
callback=self._scan_finished_cb) |
437 |
self._scan_thread.start() |
438 |
self._setup_widgets() |
439 |
self._init_scanner() |
440 |
|
441 |
@trace() |
442 |
def destroy(self): |
443 |
self._scan_thread.stop_thread() |
444 |
self._scan_thread.join() |
445 |
logging.debug('ScanThread finished') |
446 |
[info['file'].close() for info in self._image_infos] |
447 |
activity.Activity.destroy(self) |
448 |
|
449 |
def read_file(self, file_path): |
450 |
if not self._scanners: |
451 |
return |
452 |
|
453 |
globals_dict = {'__builtins__': None} |
454 |
settings = eval(file(file_path).read(), globals_dict) |
455 |
self._set_settings(settings) |
456 |
# TODO: don't call set_scanner() twice when resuming (once in |
457 |
# _init_scanner() and once here) |
458 |
self._settings_toolbar.set_scanner(self._scanners[0]) |
459 |
|
460 |
def write_file(self, file_path): |
461 |
self.metadata['mime_type'] = 'text/plain' |
462 |
with file(file_path, 'w') as f: |
463 |
f.write(repr(self._get_settings())) |
464 |
|
465 |
@trace() |
466 |
def _get_settings(self): |
467 |
if not self._scanners: |
468 |
return {} |
469 |
|
470 |
settings = {} |
471 |
scanner = self._scanners[0] |
472 |
for py_name in scanner.optlist: |
473 |
logging.debug('getting value for setting %r', py_name) |
474 |
option = scanner[py_name] |
475 |
if option.type in [sane.TYPE_BOOL, sane.TYPE_INT, |
476 |
sane.TYPE_FIXED, sane.TYPE_STRING] and option.is_active(): |
477 |
|
478 |
settings[py_name] = getattr(scanner, py_name) |
479 |
|
480 |
return settings |
481 |
|
482 |
@trace() |
483 |
def _set_settings(self, settings): |
484 |
if not self._scanners: |
485 |
return |
486 |
|
487 |
scanner = self._scanners[0] |
488 |
for name, value in settings.items(): |
489 |
try: |
490 |
setattr(scanner, name, value) |
491 |
|
492 |
except AttributeError: |
493 |
logging.info('Saved setting %r=%r not support by current' |
494 |
' scanner', name, value) |
495 |
|
496 |
except sane.error: |
497 |
logging.exception('Cannot set saved setting %r=%r', name, |
498 |
value) |
499 |
|
500 |
@trace() |
501 |
def _init_scanner(self): |
502 |
sane.init() |
503 |
scanner_devs = sane.get_devices() |
504 |
if not scanner_devs: |
505 |
self._set_status('no-dev') |
506 |
return |
507 |
|
508 |
self._scanner_name = scanner_devs[0][0] |
509 |
self._reopen_scanner() |
510 |
|
511 |
def _reopen_scanner(self): |
512 |
"""Reopen scanner device to work around lower layer bugs.""" |
513 |
if self._scanners: |
514 |
try: |
515 |
old_scanner = self._scanners.pop() |
516 |
old_scanner.close() |
517 |
del old_scanner |
518 |
|
519 |
except sane.error, exc: |
520 |
logging.exception('Error closing scanner') |
521 |
|
522 |
if not self._scanner_name: |
523 |
self._set_status('no-dev') |
524 |
return |
525 |
|
526 |
try: |
527 |
self._scanners.append(sane.open(self._scanner_name)) |
528 |
|
529 |
except sane.error, exc: |
530 |
logging.exception('Error opening scanner %r', self._scanner_name) |
531 |
self._show_error(exc) |
532 |
return |
533 |
|
534 |
self._settings_toolbar.set_scanner(self._scanners[0]) |
535 |
|
536 |
self._set_status('ready') |
537 |
|
538 |
def _setup_widgets(self): |
539 |
self._setup_toolbar() |
540 |
self._setup_canvas() |
541 |
|
542 |
def _setup_canvas(self): |
543 |
vbox = gtk.VBox() |
544 |
self._status_entry = gtk.Entry() |
545 |
self._status_entry.set_editable(False) |
546 |
self._status_entry.set_sensitive(False) |
547 |
vbox.pack_start(self._status_entry, expand=False) |
548 |
self._status_entry.show() |
549 |
|
550 |
msg_box = gtk.TextView() |
551 |
msg_box.set_editable(False) |
552 |
msg_box.set_wrap_mode(gtk.WRAP_WORD) |
553 |
self._msg_box_buf = msg_box.get_buffer() |
554 |
vbox.pack_start(msg_box, expand=True, fill=True) |
555 |
msg_box.show() |
556 |
self.set_canvas(vbox) |
557 |
vbox.show() |
558 |
|
559 |
def _setup_toolbar(self): |
560 |
toolbar_box = ToolbarBox() |
561 |
|
562 |
activity_button = ActivityToolbarButton(self) |
563 |
toolbar_box.toolbar.insert(activity_button, -1) |
564 |
activity_button.show() |
565 |
|
566 |
self._scan_button = ToolButton('start-scan1') |
567 |
self._scan_button.props.tooltip = _('Start scan') |
568 |
self._scan_button.connect('clicked', self._scan_button_cb) |
569 |
toolbar_box.toolbar.insert(self._scan_button, -1) |
570 |
self._scan_button.show() |
571 |
|
572 |
self._settings_toolbar = SettingsToolbar() |
573 |
settings_button = ToolbarButton() |
574 |
settings_button.props.page = self._settings_toolbar |
575 |
settings_button.props.icon_name = 'preferences-system' |
576 |
settings_button.props.label = _('Scanner settings') |
577 |
toolbar_box.toolbar.insert(settings_button, -1) |
578 |
settings_button.show() |
579 |
|
580 |
self._save_button = ToolButton('document-save') |
581 |
self._save_button.props.tooltip = _('Save collection as PDF') |
582 |
self._save_button.set_sensitive(False) |
583 |
self._save_button.connect('clicked', self._save_button_cb) |
584 |
toolbar_box.toolbar.insert(self._save_button, -1) |
585 |
self._save_button.show() |
586 |
|
587 |
separator = gtk.SeparatorToolItem() |
588 |
separator.props.draw = False |
589 |
separator.set_expand(True) |
590 |
toolbar_box.toolbar.insert(separator, -1) |
591 |
separator.show() |
592 |
|
593 |
stop_button = StopButton(self) |
594 |
toolbar_box.toolbar.insert(stop_button, -1) |
595 |
stop_button.show() |
596 |
|
597 |
self.set_toolbar_box(toolbar_box) |
598 |
toolbar_box.show() |
599 |
|
600 |
@trace() |
601 |
def _scan_button_cb(self, *args): |
602 |
if self._status == 'ready': |
603 |
self._add_msg('triggering scan') |
604 |
self._set_status('scanning') |
605 |
self._scan_thread.start_scan() |
606 |
elif self._status == 'scanning': |
607 |
self._add_msg('stopping scan') |
608 |
self._set_status('stopping') |
609 |
self._scan_thread.stop_scan() |
610 |
else: |
611 |
logging.warning('_scan_button_cb called with status=%r', |
612 |
self._status) |
613 |
|
614 |
|
615 |
@trace() |
616 |
def _scan_finished_cb(self, image_infos, exc): |
617 |
gobject.idle_add(self._scan_finished_real_cb, image_infos, exc) |
618 |
|
619 |
@trace() |
620 |
def _scan_finished_real_cb(self, image_infos, exc): |
621 |
if exc: |
622 |
self._show_error(exc) |
623 |
|
624 |
if image_infos: |
625 |
num_images = len(image_infos) |
626 |
self._add_msg(ngettext('%d page scanned', '%d pages scanned', |
627 |
num_images) % (num_images, )) |
628 |
self._save_button.set_sensitive(True) |
629 |
|
630 |
self._image_infos += image_infos |
631 |
|
632 |
if not exc: |
633 |
self._set_status('ready') |
634 |
else: |
635 |
self._reopen_scanner() |
636 |
|
637 |
@trace() |
638 |
def _save_button_cb(self, *args): |
639 |
# TODO: do processing in background (another thread?) |
640 |
save_dir = os.path.join(self.get_activity_root(), 'instance') |
641 |
fd, name = tempfile.mkstemp(dir=save_dir) |
642 |
with os.fdopen(fd, 'w') as f: |
643 |
self._save_pdf(f) |
644 |
|
645 |
jobject = datastore.create() |
646 |
jobject.metadata['mime_type'] = 'application/pdf' |
647 |
jobject.metadata['title'] = 'Scanned document' |
648 |
# TODO: add tags, description etc. |
649 |
jobject.file_path = name |
650 |
self._save_button.set_sensitive(False) |
651 |
datastore.write(jobject, transfer_ownership=True, |
652 |
reply_handler=self._save_reply_cb, |
653 |
error_handler=self._save_error_cb) |
654 |
jobject.destroy() |
655 |
|
656 |
@trace() |
657 |
def _save_pdf(self, f): |
658 |
max_width_inch = reduce(max, |
659 |
[info['width_pixel']/info['dpi'] for info in self._image_infos]) |
660 |
max_height_inch = reduce(max, |
661 |
[info['height_pixel']/info['dpi'] for info in self._image_infos]) |
662 |
canvas = reportlab.pdfgen.canvas.Canvas(f, |
663 |
pagesize=(max_width_inch*units.inch, max_height_inch*units.inch)) |
664 |
|
665 |
for info in self._image_infos: |
666 |
width_inch = float(info['width_pixel'])/info['dpi'] |
667 |
height_inch = float(info['height_pixel'])/info['dpi'] |
668 |
canvas.setFillColorRGB(1, 1, 1) |
669 |
canvas.rect(0, 0, max_width_inch*units.inch, |
670 |
max_height_inch*units.inch, fill=1) |
671 |
canvas.drawImage(info['file'].name, 0, 0, width_inch*units.inch, |
672 |
height_inch*units.inch) |
673 |
canvas.showPage() |
674 |
|
675 |
canvas.save() |
676 |
|
677 |
@trace() |
678 |
def _save_reply_cb(self, *args): |
679 |
[info['file'].close() for info in self._image_infos] |
680 |
self._image_infos = [] |
681 |
|
682 |
@trace() |
683 |
def _save_error_cb(self, error): |
684 |
self._show_error(error) |
685 |
self._save_button.set_sensitive(True) |
686 |
|
687 |
@trace() |
688 |
def _show_error(self, exc): |
689 |
self._add_msg(exc) |
690 |
self._set_status('error', exc) |
691 |
|
692 |
@trace() |
693 |
def _add_msg(self, msg): |
694 |
logging.info('%s', msg) |
695 |
self._msg_box_buf.insert(self._msg_box_buf.get_end_iter(), |
696 |
'%.1f %s\n' % (time.time(), msg)) |
697 |
|
698 |
@trace() |
699 |
def _set_status(self, status, *args): |
700 |
self._status = status |
701 |
self._status_entry.set_text(self._STATUS_MSGS[status] % tuple(args)) |
702 |
|
703 |
if self._status == 'ready': |
704 |
self._scan_button.set_icon('start-scan1') |
705 |
self._scan_button.props.tooltip = _('Start scan') |
706 |
self._scan_button.set_sensitive(True) |
707 |
elif self._status == 'scanning': |
708 |
self._scan_button.set_icon('stop-scan1') |
709 |
self._scan_button.props.tooltip = _('Stop scan') |
710 |
self._scan_button.set_sensitive(True) |
711 |
else: |
712 |
self._scan_button.set_sensitive(False) |
713 |
|
714 |
|
715 |
def start(): |
716 |
gtk.gdk.threads_init() |
717 |
gtk.gdk.threads_enter() |
718 |
|
719 |
# FIXME: can't call gtk.gdk.threads_leave() because sugar-activity |
720 |
# (sugar.activity.main.main()) doesn't provide a hook for that. :( |