#!/usr/bin/env python # # Author: Sascha Silbe # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3 # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Scan: Import images using a document scanner. """ from __future__ import with_statement from StringIO import StringIO from gettext import lgettext as _ from gettext import lngettext as ngettext import logging import math import operator import os import subprocess import sys import tempfile import threading import time import gobject import gtk from PIL import ImageOps import sane from sugar.activity.widgets import ActivityToolbarButton, StopButton from sugar.graphics.toggletoolbutton import ToggleToolButton from sugar.activity import activity from sugar.datastore import datastore from sugar.graphics.toolbutton import ToolButton from sugar.graphics.combobox import ComboBox from sugar.graphics.toolbarbox import ToolbarButton, ToolbarBox from sugar.graphics import style from sugar.logger import trace import postproc class SettingsToolbar(gtk.ToolPalette): @trace() def __init__(self): gtk.ToolPalette.__init__(self) self.autocontrast = False self.autocontrast_cutoff = 0 self.autocrop_threshold = True self._scanner = None self._widget_by_option = {} self._toolitem_by_option = {} self._updating = False self._static_tool_group = None self._add_static_widgets() def modify_bg(self, state, color): gtk.ToolPalette.modify_bg(self, state, color) for group in self: group.modify_bg(state, color) def set_scanner(self, scanner): self._scanner = scanner self._clear() if self._scanner is not None: self._add_options() def _add_static_widgets(self): self._static_tool_group = gtk.ToolItemGroup(_('Post-processing')) self._static_tool_group.show() self.add(self._static_tool_group) self.set_expand(self._static_tool_group, False) tool_item = self._create_spin_toolitem('autocontrast_cutoff', _('Automatic contrast cut-off threshold'), 0, 100, _('Percentage of upper and lower' ' pixel values to eliminate' ' (clamp to lowest resp.' ' highest value).')) tool_item.show() self._static_tool_group.insert(tool_item, -1) tool_item = self._create_spin_toolitem('autocrop_threshold', _('Auto-crop threshold'), 0, 50, _('Ignore the upper / lower pixel values for auto-cropping')) tool_item.show() self._static_tool_group.insert(tool_item, -1) def _create_spin_toolitem(self, name, label, min, max, tooltip): label = gtk.Label(label) label.modify_fg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) value = getattr(self, name) spin_adj = gtk.Adjustment(value, min, max, 1, max//10, 0) spin = gtk.SpinButton(spin_adj, 0, 0) spin.props.snap_to_ticks = True spin.set_numeric(True) spin.connect('value-changed', lambda widget: setattr(self, name, int(widget.get_value()))) spin.show() hbox = gtk.HBox(False, style.DEFAULT_SPACING) hbox.pack_start(label, False) hbox.pack_start(spin) label.show() tool_item = gtk.ToolItem() tool_item.add(hbox) hbox.show() tool_item.set_tooltip_text(tooltip) return tool_item def _clear(self): self._widget_by_option = {} self._toolitem_by_option = {} for widget in list(self): if widget == self._static_tool_group: continue self.remove(widget) @trace() def _get_groups(self): group = [] groups = [(None, group)] options = self._scanner.get_options() for idx, name, title, dsc_, _type, unit_, siz_, cap_, cons_ in options: if _type == sane.TYPE_GROUP: group = [] groups.append((title, group)) continue if not name and idx != 0: logging.warning('ignoring unnamed option (index %d)', idx) continue group.append(name) return groups def _add_options(self): groups = self._get_groups() logging.debug('SettingsToolbar._add_options(): groups=%r', groups) name_map = dict([(self._scanner[py_name].name, py_name) for py_name in self._scanner.optlist]) first_group = True for group_name, group in groups: if not group: continue tool_group = gtk.ToolItemGroup(group_name) tool_group.show() self.add(tool_group) self.set_expand(tool_group, False) # TODO: handle SANE_CAP_ADVANCED and maybe other capabilities # TODO: handle area by paper size presets + single fields in # advanced options for option_name in group: py_name = name_map[option_name] option = self._scanner[py_name] if (option.type == sane.TYPE_BOOL): widget = self._add_bool(option) elif (option.type == sane.TYPE_INT): widget = self._add_integer(option) elif (option.type == sane.TYPE_FIXED): widget = self._add_fixed_point(option) elif (option.type == sane.TYPE_STRING): widget = self._add_string(option) elif (option.type == sane.TYPE_BUTTON): widget = self._add_action(option) else: logging.warning('Skipping setting %r of unknown type %r', option_name, option.type) continue if not widget: logging.warning('Skipping setting %r (type %r) without' ' widget', option_name, option.type) continue widget.show() # FIXME: take translation from SANE label = gtk.Label(_(option.title)) label.modify_fg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color()) hbox = gtk.HBox(False, style.DEFAULT_SPACING) hbox.pack_start(label, False) hbox.pack_start(widget) label.show() tool_item = gtk.ToolItem() tool_item.add(hbox) hbox.show() # FIXME: take translation from SANE tool_item.set_tooltip_text(_(option.desc)) # TODO: represent unit tool_group.insert(tool_item, -1) logging.debug('%r size request: %r', tool_group, tool_group.size_request()) self._widget_by_option[py_name] = widget self._toolitem_by_option[py_name] = tool_item self._update_state() self.show() def _update_size(self): # HACK to work around GTK not calculating the size of # ToolItemGroups (and thus this ToolPalette) correctly. See # http://mail.gnome.org/archives/gtkmm-list/2011-September/msg00112.html tool_groups_height = 0 for tool_group in self: max_height = reduce(max, [tool_item.size_request()[1] for tool_item in tool_group], 0) header_height = tool_group.size_request()[1] tool_groups_height += max_height + header_height self.set_size_request(-1, tool_groups_height) def _add_bool(self, option): button = gtk.CheckButton() button.connect('toggled', lambda widget, option=option: self._toggle_cb(widget, option)) return button def _add_integer(self, option): if isinstance(option.constraint, tuple): return self._add_spinbutton(option) elif isinstance(option.constraint, list): return self._add_combo(option) return self._add_spinbutton(option) def _add_fixed_point(self, option): if isinstance(option.constraint, tuple): return self._add_spinbutton(option) elif isinstance(option.constraint, list): # TODO return None else: # TODO return None @trace() def _add_string(self, option): if option.constraint: return self._add_combo(option) return self._add_entry(option) def _add_action(self, option): # TODO return None def _add_spinbutton(self, option): if isinstance(option.constraint, tuple): lower, upper, step = option.constraint else: lower, upper, step = 0, sys.maxint, 1 if not step: # no quantization set => guess it step = 1 spin_adj = gtk.Adjustment(lower, lower, upper, step, step*10, 0) spin = gtk.SpinButton(spin_adj, 0, 0) spin.props.snap_to_ticks = True spin.set_digits(self._calc_digits(step)) if option.type == sane.TYPE_INT: spin.connect('value-changed', lambda widget, option=option: self._spin_int_changed_cb(widget, option)) else: spin.connect('value-changed', lambda widget, option=option: self._spin_float_changed_cb(widget, option)) spin.set_numeric(True) spin.show() return spin def _calc_digits(self, step): digits = 0 while math.modf(step)[0] > .001: digits += 1 step *= 10 return digits def _add_entry(self, option): # untested entry = gtk.Entry() entry.connect('focus-out-event', lambda widget, option=option: self._entry_changed_cb(widget, option)) entry.connect('activated', lambda widget, option=option: self._entry_changed_cb(widget, option)) return entry @trace() def _add_combo(self, option): box = ComboBox() for combo_value in option.constraint: # FIXME: take translation from SANE box.append_item(combo_value, _(combo_value)) box.connect('changed', lambda widget, option=option: self._combo_changed_cb(widget, option)) return box @trace() def _toggle_cb(self, button, option): if self._updating: return new_value = button.get_active() setattr(self._scanner, option.py_name, new_value) self._update_state() @trace() def _entry_changed_cb(self, entry, option): if self._updating: return setattr(self._scanner, option.py_name, entry.get_text()) self._update_state() @trace() def _combo_changed_cb(self, combo, option): if self._updating: return setattr(self._scanner, option.py_name, combo.get_value()) self._update_state() @trace() def _spin_float_changed_cb(self, spin, option): if self._updating: return setattr(self._scanner, option.py_name, spin.get_value()) self._update_state() @trace() def _spin_int_changed_cb(self, spin, option): if self._updating: return setattr(self._scanner, option.py_name, int(spin.get_value())) self._update_state() def _update_state(self): """(Re-)check SANE attributes after changes were made (or on start-up) Some SANE options are interdependent, so we need to update attributes after every change. E.g. the JPEG compression ratio (integer) is only available if JPEG compression (bool) is enabled. In addition, the current value is only available for "active" options, so we can't set the values of widgets for inactive options. """ try: self._updating = True for py_option_name, widget in self._widget_by_option.items(): toolitem = self._toolitem_by_option[py_option_name] option = self._scanner[py_option_name] widget.set_sensitive(option.is_settable()) if not option.is_active(): toolitem.hide() continue value = getattr(self._scanner, py_option_name) if isinstance(widget, gtk.CheckButton): widget.set_active(value) elif isinstance(widget, gtk.SpinButton): widget.set_value(value) elif isinstance(widget, gtk.Entry): widget.set_text(value) elif isinstance(widget, ComboBox): new_idx = option.constraint.index(value) widget.set_active(new_idx) toolitem.show() self._update_size() finally: self._updating = False class ScanThread(threading.Thread): def __init__(self, temp_dir, callback=None): threading.Thread.__init__(self) self._device = None self._temp_dir = temp_dir self._callback = callback self._cond = threading.Condition() self._action = None self._images = [] self._error = None self._ready = False self._dpi = None self.autocontrast = True self.autocontrast_cutoff = 0 self.autocrop_threshold = 0 def set_device(self, device): with self._cond: self._device = device def run(self): while True: with self._cond: while not self._action: logging.debug('waiting') self._cond.wait() logging.debug('woken') action = self._action self._action = None if action == 'quit': return if action == 'start': self._scan() def stop_thread(self): with self._cond: self._action = 'quit' self._cond.notify() @trace() def start_scan(self): with self._cond: if self._device is None: raise ValueError('No device set') logging.debug('start_scan(): got lock') self._action = 'start' self._cond.notify() logging.debug('start_scan(): notified') logging.debug('start_scan(): lock released') @trace() def stop_scan(self): with self._cond: logging.debug('stop_scan(): got lock') self._action = 'stop' self._cond.notify() logging.debug('stop_scan(): notified') logging.debug('stop_scan(): lock released') def is_ready(self): with self._cond: return self._ready def get_result(self): with self._cond: if self._error: raise self._error images = self._images self._images = [] return images @trace() def _scan(self): self._images = None self._error = None source = getattr(self._device, 'source', 'Auto') if getattr(self._device, 'batch_scan', False): scan_multi = source in ['Auto', 'ADF'] else: scan_multi = source == 'ADF' try: if scan_multi: self._scan_multi() else: logging.debug('starting single scan') self._dpi = self._device.resolution self._images = [self._process_image(self._device.scan())] except sane.error, exc: # Ouch. sane.error doesn't derive from Exception :-/ self._error = ValueError('SANE error: %s' % (exc, )) except Exception, exc: self._error = exc with self._cond: self._ready = True try: self._callback(self._images, self._error) except: logging.exception('_scan(): Uncaught exception in callback') @trace() def _scan_multi(self): logging.debug('_scan: calling multi_scan()') scan_iter = self._device.multi_scan() logging.debug('_scan: acquiring images') images = [] try: self._dpi = self._device.resolution try: for image in scan_iter: images.append(self._process_image(image)) if self._check_action(['stop', 'quit']): break self._dpi = self._device.resolution except sane.error, exc: # Work around python-sane bug. if str(exc) != 'Document feeder out of documents': raise self._images = images finally: logging.debug('_scan_multi(): closing iterator') # make sure the scan is "cancelled" _now_ del scan_iter logging.debug('_scan_multi(): iterator closed') @trace() def _check_action(self, actions): with self._cond: return self._action in actions def _pil_image_to_pixbuf(self, image): f = StringIO() image.save(f, 'ppm') contents = f.getvalue() f.close() loader = gtk.gdk.PixbufLoader('pnm') loader.write(contents, len(contents)) pixbuf = loader.get_pixbuf() loader.close() return pixbuf def _process_image(self, image): logging.debug('pre-crop size: %r', image.size) if self.autocontrast: image = ImageOps.autocontrast(image, self.autocontrast_cutoff) image = postproc.autocrop(image, self.autocrop_threshold) # image = postproc.autocrop(image, self.autocrop_open_threshold, # self.autocrop_close_threshold, # self.autocrop_nth_largest) logging.debug('post-crop size: %r', image.size) image_file = tempfile.NamedTemporaryFile(dir=self._temp_dir, suffix='.png') image.save(image_file.name) image_file.flush() size = image.size image.thumbnail((1024, 1024)) preview = self._pil_image_to_pixbuf(image) info = { 'file': image_file, 'width_pixel': size[0], 'height_pixel': size[1], 'dpi': self._dpi, 'preview': preview, } del image return info class ScanActivity(activity.Activity): _STATUS_MSGS = { 'no-dev': _('No scanner found'), 'ready': _('Ready'), 'stopping': _('Stopping...'), 'scanning': _('Scanning...'), 'error': _('Error: %s'), } _MODEL_COLUMNS = [ ('preview', gtk.gdk.Pixbuf), ('file', object), ('width_pixel', int), ('height_pixel', int), ('dpi', int), ] _MODEL_COLUMNS_MAP = dict([(name, idx) for idx, (name, type_) in enumerate(_MODEL_COLUMNS)]) def __init__(self, handle): activity.Activity.__init__(self, handle) self.max_participants = 1 # content: (device_name, vendor, model, description) self._scanner_infos = [] self._current_scanner = None self._current_scanner_name = None self._scanner_options = {'source': 'ADF'} self._status = 'init' self._msg_box_buf = None self._status_entry = None self._scan_button = None self._settings_toolbar = None self._old_settings = None temp_dir = os.path.join(self.get_activity_root(), 'tmp') self._scan_thread = ScanThread(temp_dir, callback=self._scan_finished_cb) self._scan_thread.start() self._setup_widgets() # TODO: delay opening scanner device if we're resuming a # previous session self._init_scanner() @trace() def destroy(self): self._scan_thread.stop_thread() self._scan_thread.join() logging.debug('ScanThread finished') self._remove_image_files() activity.Activity.destroy(self) def read_file(self, file_path): if not self._scanner_infos: # no scanners available, nothing to do for now (see TODO # item in write_file()) return globals_dict = {'__builtins__': None} settings = eval(file(file_path).read(), globals_dict) logging.debug('old settings: %r', settings) scanner_name = settings.pop('_scanner_name', None) if not scanner_name: # Saved by a previous version, just pick the first # available scanner like the previous version did. scanner_name = self._scanner_infos[0][0] if scanner_name not in [info[0] for info in self._scanner_infos]: # The previous scanner is not available right now, pick # another one and apply as much of the previous settings # as we can. scanner_name = self._scanner_infos[0][0] self._open_scanner(scanner_name) self._set_settings(settings) # HACK: cause the toolbar to re-read the settings after # changing them self._settings_toolbar.set_scanner(self._current_scanner) def write_file(self, file_path): # TODO: retain settings for all scanners, not just the current one self.metadata['mime_type'] = 'text/plain' with file(file_path, 'w') as f: f.write(repr(self._get_settings())) @trace() def _get_settings(self): if self._current_scanner is None: return {} settings = {'_scanner_name': self._current_scanner_name} for py_name in self._current_scanner.optlist: logging.debug('getting value for setting %r', py_name) option = self._current_scanner[py_name] if option.type in [sane.TYPE_BOOL, sane.TYPE_INT, sane.TYPE_FIXED, sane.TYPE_STRING] and option.is_active(): settings[py_name] = getattr(self._current_scanner, py_name) return settings @trace() def _set_settings(self, settings): if self._current_scanner is None: return for name, value in settings.items(): try: setattr(self._current_scanner, name, value) except AttributeError: logging.info('Saved setting %r=%r not support by current' ' scanner', name, value) except sane.error: logging.exception('Cannot set saved setting %r=%r', name, value) @trace() def _init_scanner(self): sane.init() self._scanner_infos = sane.get_devices() if not self._scanner_infos: self._set_status('no-dev') return self._open_scanner(self._scanner_infos[0][0]) def _open_scanner(self, name): """Open the current scanner device Close the previous device if there is one, but ignore errors while closing. Open the device identified by name. Can be used to open the scanner device on start-up, change the current scanner or reopen the scanner device to work around lower layer bugs. """ if self._current_scanner: old_scanner, self._current_scanner = self._current_scanner, None try: old_scanner.close() del old_scanner except sane.error, exc: logging.exception('Error closing scanner') if not name: self._set_status('no-dev') self._set_scanner(None, None) return try: scanner = sane.open(name) except sane.error, exc: logging.exception('Error opening scanner %r', name) self._show_error(exc) return self._set_scanner(scanner, name) self._set_status('ready') @trace() def _set_scanner(self, device, name): self._current_scanner = device self._current_scanner_name = name self._scan_thread.set_device(device) self._settings_toolbar.set_scanner(device) def _setup_widgets(self): self._setup_toolbar() self._setup_canvas() def _setup_canvas(self): vbox = gtk.VBox() self._status_entry = gtk.Entry() self._status_entry.set_editable(False) self._status_entry.set_sensitive(False) vbox.pack_start(self._status_entry, expand=False) self._status_entry.show() metadata_box = gtk.Table(2, 2, False) vbox.pack_start(metadata_box, expand=False) # TRANSLATORS: The character immediately following the underscore # is taken as the "mnemonic" (keyboard shortcut) associated with # this label. You need to make sure all labels have different # mnemonics associated with them. E.g. in english, T_itle and # T_ags (and therefore +I resp. +A) is used to resolve # a conflict between those two: using the first character for # both of them (i.e. _Title and _Tags) would result in the same # shortcut +T. title_label = gtk.Label(_('T_itle:')) title_label.set_use_underline(True) metadata_box.attach(title_label, 0, 1, 0, 1, xoptions=0, xpadding=10) self._title_entry = gtk.Entry() self._title_entry.set_text(_('Scanned document')) title_label.set_mnemonic_widget(self._title_entry) metadata_box.attach(self._title_entry, 1, 2, 0, 1) # TRANSLATORS: The underscore indicates a mnemonic. See the # comment of 'T_itle' for more information. tags_label = gtk.Label(_('T_ags:')) tags_label.set_use_underline(True) metadata_box.attach(tags_label, 0, 1, 1, 2, xoptions=0, xpadding=10) self._tags_entry = gtk.Entry() tags_label.set_mnemonic_widget(self._tags_entry) metadata_box.attach(self._tags_entry, 1, 2, 1, 2) metadata_box.show_all() msg_box = gtk.TextView() msg_box.set_editable(False) msg_box.set_wrap_mode(gtk.WRAP_WORD) self._msg_box_buf = msg_box.get_buffer() #vbox.pack_start(msg_box, expand=False) #msg_box.show() images_window = gtk.ScrolledWindow() images_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) model_types = [_type for name_, _type in self._MODEL_COLUMNS] self._images_model = gtk.ListStore(*model_types) self._images_view = gtk.TreeView(self._images_model) selection = self._images_view.get_selection() selection.connect('changed', self._selection_changed_cb) selection.set_mode(gtk.SELECTION_MULTIPLE) image_column = gtk.TreeViewColumn('Image', gtk.CellRendererPixbuf(), pixbuf=0) self._images_view.append_column(image_column) self._images_view.set_headers_visible(False) self._images_view.set_reorderable(True) images_window.add(self._images_view) images_window.show_all() vbox.pack_start(images_window, expand=True) self.set_canvas(vbox) vbox.show() def _setup_toolbar(self): toolbar_box = ToolbarBox() activity_button = ActivityToolbarButton(self) toolbar_box.toolbar.insert(activity_button, -1) activity_button.show() self._scan_button = ToolButton('start-scan1') self._scan_button.accelerator = 'S' self._scan_button.props.tooltip = _('Start scan') self._scan_button.connect('clicked', self._scan_button_cb) toolbar_box.toolbar.insert(self._scan_button, -1) self._scan_button.show() self._settings_toolbar = SettingsToolbar() settings_button = ToolbarButton() settings_button.props.icon_name = 'preferences-system' settings_button.props.label = _('Scanner settings') settings_button.props.page = self._settings_toolbar toolbar_box.toolbar.insert(settings_button, -1) settings_button.show() # HACK to work around fixed size of Sugar palettes def toolbar_button_realize_cb(*args): width, height = self._settings_toolbar.size_request() height += 10 settings_button.page_widget.set_size_request(width, height) settings_button.connect('realize', lambda *args: gobject.idle_add(toolbar_button_realize_cb)) self._remove_button = ToolButton('edit-delete') self._remove_button.props.tooltip = _('Remove selected page(s)') self._remove_button.set_sensitive(False) self._remove_button.connect('clicked', self._remove_button_cb) toolbar_box.toolbar.insert(self._remove_button, -1) self._remove_button.show() self._save_button = ToolButton('document-save') self._save_button.accelerator = 'P' self._save_button.props.tooltip = _('Save collection as PDF') self._save_button.set_sensitive(False) self._save_button.connect('clicked', self._save_button_cb) toolbar_box.toolbar.insert(self._save_button, -1) self._save_button.show() separator = gtk.SeparatorToolItem() separator.props.draw = False separator.set_expand(True) toolbar_box.toolbar.insert(separator, -1) separator.show() stop_button = StopButton(self) toolbar_box.toolbar.insert(stop_button, -1) stop_button.show() self.set_toolbar_box(toolbar_box) toolbar_box.show() @trace() def _scan_button_cb(self, *args): if self._status == 'ready': self._add_msg('triggering scan') self._set_status('scanning') self._scan_thread.autocontrast = self._settings_toolbar.autocontrast self._scan_thread.autocontrast_cutoff = self._settings_toolbar.autocontrast_cutoff self._scan_thread.autocrop_threshold = self._settings_toolbar.autocrop_threshold # Remember current settings as we need to restore them # when re-opening the scanner after the scan. self._old_settings = self._get_settings() self._scan_thread.start_scan() elif self._status == 'scanning': self._add_msg('stopping scan') self._set_status('stopping') self._scan_thread.stop_scan() else: logging.warning('_scan_button_cb called with status=%r', self._status) @trace() def _scan_finished_cb(self, image_infos, exc): gobject.idle_add(self._scan_finished_real_cb, image_infos, exc) @trace() def _scan_finished_real_cb(self, image_infos, exc): if exc: self._show_error(exc) if image_infos: num_images = len(image_infos) self._add_msg(ngettext('%d page scanned', '%d pages scanned', num_images) % (num_images, )) self._save_button.set_sensitive(True) self._add_images(image_infos) if not exc: self._set_status('ready') # always re-open scanner, to work around HPLIP bugs self._open_scanner(self._current_scanner_name) self._set_settings(self._old_settings) # HACK: cause the toolbar to re-read the settings after # changing them self._settings_toolbar.set_scanner(self._current_scanner) def _add_images(self, image_infos): for info in image_infos: row = [info[name] for name, type_ in self._MODEL_COLUMNS] self._images_model.append(row) def _selection_changed_cb(self, selection): selected = bool(selection.count_selected_rows()) self._remove_button.set_sensitive(selected) def _remove_button_cb(self, button): model_, paths = self._images_view.get_selection().get_selected_rows() refs = [gtk.TreeRowReference(self._images_model, path) for path in paths] for ref in refs: iter = self._images_model.get_iter(ref.get_path()) self._images_model.remove(iter) @trace() def _save_button_cb(self, *args): # TODO: do processing in background (another thread?) save_dir = os.path.join(self.get_activity_root(), 'instance') fd, file_name = tempfile.mkstemp(dir=save_dir) os.close(fd) self._save_pdf(file_name) jobject = datastore.create() jobject.metadata['mime_type'] = 'application/pdf' jobject.metadata['title'] = self._title_entry.get_text() jobject.metadata['tags'] = self._tags_entry.get_text() jobject.file_path = file_name self._save_button.set_sensitive(False) # async not supported, see SL#3071 #datastore.write(jobject, transfer_ownership=True, # reply_handler=self._save_reply_cb, # error_handler=self._save_error_cb) try: datastore.write(jobject, transfer_ownership=True) except Exception, exception: self._save_error_cb(exception) else: self._save_reply_cb() jobject.destroy() @trace() def _save_pdf(self, file_name): width_idx = self._MODEL_COLUMNS_MAP['width_pixel'] height_idx = self._MODEL_COLUMNS_MAP['height_pixel'] dpi_idx = self._MODEL_COLUMNS_MAP['dpi'] file_idx = self._MODEL_COLUMNS_MAP['file'] options = ['-units', 'PixelsPerInch'] image_options = [['-density', '%dx%d' % (row[dpi_idx], row[dpi_idx]), row[file_idx].name] for row in self._images_model] options += reduce(operator.add, image_options) options += ['-compress', 'Zip', 'PDF:' + file_name] logging.debug('Invoking %r', ['convert'] + options) subprocess.check_call(['convert'] + options) def _remove_image_files(self): for row in self._images_model: row[self._MODEL_COLUMNS_MAP['file']].close() @trace() def _save_reply_cb(self, *args): self._remove_image_files() self._images_model.clear() @trace() def _save_error_cb(self, error): self._show_error(error) self._save_button.set_sensitive(True) @trace() def _show_error(self, exc): self._add_msg(exc) self._set_status('error', exc) @trace() def _add_msg(self, msg): logging.info('%s', msg) self._msg_box_buf.insert(self._msg_box_buf.get_end_iter(), '%.1f %s\n' % (time.time(), msg)) @trace() def _set_status(self, status, *args): self._status = status self._status_entry.set_text(self._STATUS_MSGS[status] % tuple(args)) if self._status == 'ready': self._scan_button.set_icon('start-scan1') self._scan_button.props.tooltip = _('Start scan') self._scan_button.set_sensitive(True) elif self._status == 'scanning': self._scan_button.set_icon('stop-scan1') self._scan_button.props.tooltip = _('Stop scan') self._scan_button.set_sensitive(True) else: self._scan_button.set_sensitive(False) def find_executable(name): for directory in os.environ['PATH'].split(':'): path = os.path.join(directory, name) if os.access(path, os.X_OK): return path return None def start(): gtk.gdk.threads_init() gtk.gdk.threads_enter() # FIXME: can't call gtk.gdk.threads_leave() because sugar-activity # (sugar.activity.main.main()) doesn't provide a hook for that. :(