diff options
Diffstat (limited to 'src/simple-scan.vala')
-rw-r--r-- | src/simple-scan.vala | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/src/simple-scan.vala b/src/simple-scan.vala new file mode 100644 index 0000000..813f29b --- /dev/null +++ b/src/simple-scan.vala @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2009-2011 Canonical Ltd. + * Author: Robert Ancell <robert.ancell@canonical.com> + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See http://www.gnu.org/copyleft/gpl.html the full text of the + * license. + */ + +public class SimpleScan : Gtk.Application +{ + static bool show_version; + static bool debug_enabled; + static string? fix_pdf_filename = null; + public static const OptionEntry[] options = + { + { "version", 'v', 0, OptionArg.NONE, ref show_version, + /* Help string for command line --version flag */ + N_("Show release version"), null}, + { "debug", 'd', 0, OptionArg.NONE, ref debug_enabled, + /* Help string for command line --debug flag */ + N_("Print debugging messages"), null}, + { "fix-pdf", 0, 0, OptionArg.STRING, ref fix_pdf_filename, + N_("Fix PDF files generated with older versions of Simple Scan"), "FILENAME..."}, + { null } + }; + private static Timer log_timer; + private static FileStream? log_file; + + private ScanDevice? default_device = null; + private bool have_devices = false; + private GUdev.Client udev_client; + private UserInterface ui; + private Scanner scanner; + private Book book; + + public SimpleScan (ScanDevice? device = null) + { + default_device = device; + } + + public override void startup () + { + base.startup (); + + ui = new UserInterface (); + book = ui.book; + ui.start_scan.connect (scan_cb); + ui.stop_scan.connect (cancel_cb); + ui.email.connect (email_cb); + + scanner = Scanner.get_instance (); + scanner.update_devices.connect (update_scan_devices_cb); + scanner.request_authorization.connect (authorize_cb); + scanner.expect_page.connect (scanner_new_page_cb); + scanner.got_page_info.connect (scanner_page_info_cb); + scanner.got_line.connect (scanner_line_cb); + scanner.page_done.connect (scanner_page_done_cb); + scanner.document_done.connect (scanner_document_done_cb); + scanner.scan_failed.connect (scanner_failed_cb); + scanner.scanning_changed.connect (scanner_scanning_changed_cb); + + string[]? subsystems = { "usb", null }; + udev_client = new GUdev.Client (subsystems); + udev_client.uevent.connect (on_uevent); + + if (default_device != null) + { + List<ScanDevice> device_list = null; + + device_list.append (default_device); + ui.set_scan_devices (device_list); + ui.selected_device = default_device.name; + } + } + + public override void activate () + { + base.activate (); + ui.start (); + scanner.start (); + } + + public override void shutdown () + { + base.shutdown (); + book = null; + ui = null; + udev_client = null; + scanner.free (); + } + + private void update_scan_devices_cb (Scanner scanner, List<ScanDevice> devices) + { + var devices_copy = devices.copy (); + + /* If the default device is not detected add it to the list */ + if (default_device != null) + { + var default_in_list = false; + foreach (var device in devices_copy) + { + if (device.name == default_device.name) + { + default_in_list = true; + break; + } + } + + if (!default_in_list) + devices_copy.prepend (default_device); + } + + have_devices = devices_copy.length () > 0; + ui.set_scan_devices (devices_copy); + } + + private void authorize_cb (Scanner scanner, string resource) + { + string username, password; + ui.authorize (resource, out username, out password); + scanner.authorize (username, password); + } + + private Page append_page () + { + /* Use current page if not used */ + var page = book.get_page (-1); + if (page != null && !page.has_data) + { + ui.selected_page = page; + page.start (); + return page; + } + + /* Copy info from previous page */ + var scan_direction = ScanDirection.TOP_TO_BOTTOM; + bool do_crop = false; + string named_crop = null; + var width = 100, height = 100, dpi = 100, cx = 0, cy = 0, cw = 0, ch = 0; + if (page != null) + { + scan_direction = page.scan_direction; + width = page.width; + height = page.height; + dpi = page.dpi; + + do_crop = page.has_crop; + if (do_crop) + { + named_crop = page.crop_name; + cx = page.crop_x; + cy = page.crop_y; + cw = page.crop_width; + ch = page.crop_height; + } + } + + page = new Page (width, height, dpi, scan_direction); + book.append_page (page); + if (do_crop) + { + if (named_crop != null) + { + page.set_named_crop (named_crop); + } + else + page.set_custom_crop (cw, ch); + page.move_crop (cx, cy); + } + ui.selected_page = page; + page.start (); + + return page; + } + + private void scanner_new_page_cb (Scanner scanner) + { + append_page (); + } + + private string? get_profile_for_device (string device_name) + { +#if HAVE_COLORD + var device_id = "sane:%s".printf (device_name); + debug ("Getting color profile for device %s", device_name); + + var client = new Colord.Client (); + try + { + client.connect_sync (); + } + catch (Error e) + { + debug ("Failed to connect to colord: %s", e.message); + return null; + } + + Colord.Device device; + try + { + device = client.find_device_by_property_sync (Colord.DEVICE_PROPERTY_SERIAL, device_id); + } + catch (Error e) + { + debug ("Unable to find colord device %s: %s", device_name, e.message); + return null; + } + + try + { + device.connect_sync (); + } + catch (Error e) + { + debug ("Failed to get properties from the device %s: %s", device_name, e.message); + return null; + } + + var profile = device.get_default_profile (); + if (profile == null) + { + debug ("No default color profile for device: %s", device_name); + return null; + } + + try + { + profile.connect_sync (); + } + catch (Error e) + { + debug ("Failed to get properties from the profile %s: %s", device_name, e.message); + return null; + } + + if (profile.filename == null) + { + debug ("No icc color profile for the device %s", device_name); + return null; + } + + debug ("Using color profile %s for device %s", profile.filename, device_name); + return profile.filename; +#else + return null; +#endif + } + + private void scanner_page_info_cb (Scanner scanner, ScanPageInfo info) + { + debug ("Page is %d pixels wide, %d pixels high, %d bits per pixel", + info.width, info.height, info.depth); + + /* Add a new page */ + var page = append_page (); + page.set_page_info (info); + + /* Get ICC color profile */ + /* FIXME: The ICC profile could change */ + /* FIXME: Don't do a D-bus call for each page, cache color profiles */ + page.color_profile = get_profile_for_device (info.device); + } + + private void scanner_line_cb (Scanner scanner, ScanLine line) + { + var page = book.get_page ((int) book.n_pages - 1); + page.parse_scan_line (line); + } + + private void scanner_page_done_cb (Scanner scanner) + { + var page = book.get_page ((int) book.n_pages - 1); + page.finish (); + } + + private void remove_empty_page () + { + var page = book.get_page ((int) book.n_pages - 1); + if (!page.has_data) + book.delete_page (page); + } + + private void scanner_document_done_cb (Scanner scanner) + { + remove_empty_page (); + } + + private void scanner_failed_cb (Scanner scanner, int error_code, string error_string) + { + remove_empty_page (); + if (error_code != Sane.Status.CANCELLED) + { + ui.show_error (/* Title of error dialog when scan failed */ + _("Failed to scan"), + error_string, + have_devices); + } + } + + private void scanner_scanning_changed_cb (Scanner scanner) + { + ui.scanning = scanner.is_scanning (); + } + + private void scan_cb (UserInterface ui, string? device, ScanOptions options) + { + debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device); + + if (!scanner.is_scanning ()) + append_page (); + + /* Default filename to use when saving document (and extension will be added, e.g. .jpg) */ + string filename_prefix = _("Scanned Document"); + string extension; + if (options.scan_mode == ScanMode.COLOR) + extension = "jpg"; + else + extension = "pdf"; + var filename = "%s.%s".printf (filename_prefix, extension); + ui.default_file_name = filename; + scanner.scan (device, options); + } + + private void cancel_cb (UserInterface ui) + { + scanner.cancel (); + } + + private string? get_temporary_filename (string prefix, string extension) + { + /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and + * use the filename but it appears to work in practise */ + + var filename = "%sXXXXXX.%s".printf (prefix, extension); + string path; + try + { + var fd = FileUtils.open_tmp (filename, out path); + Posix.close (fd); + } + catch (Error e) + { + warning ("Error saving email attachment: %s", e.message); + return null; + } + + return path; + } + + private void email_cb (UserInterface ui, string profile, int quality) + { + var saved = false; + var command_line = "xdg-email"; + + /* Save text files as PDFs */ + if (profile == "text") + { + /* Open a temporary file */ + var path = get_temporary_filename ("scan", "pdf"); + if (path != null) + { + var file = File.new_for_path (path); + ui.show_progress_dialog (); + try + { + book.save ("pdf", quality, file); + } + catch (Error e) + { + ui.hide_progress_dialog (); + warning ("Unable to save email file: %s", e.message); + return; + } + command_line += " --attach %s".printf (path); + } + } + else + { + for (var i = 0; i < book.n_pages; i++) + { + var path = get_temporary_filename ("scan", "jpg"); + if (path == null) + { + saved = false; + break; + } + + var file = File.new_for_path (path); + try + { + book.get_page (i).save ("jpeg", quality, file); + } + catch (Error e) + { + warning ("Unable to save email file: %s", e.message); + return; + } + command_line += " --attach %s".printf (path); + + if (!saved) + break; + } + } + + debug ("Launching email client: %s", command_line); + try + { + Process.spawn_command_line_async (command_line); + } + catch (Error e) + { + warning ("Unable to start email: %s", e.message); + } + } + + private static void log_cb (string? log_domain, LogLevelFlags log_level, string message) + { + string prefix; + + switch (log_level & LogLevelFlags.LEVEL_MASK) + { + case LogLevelFlags.LEVEL_ERROR: + prefix = "ERROR:"; + break; + case LogLevelFlags.LEVEL_CRITICAL: + prefix = "CRITICAL:"; + break; + case LogLevelFlags.LEVEL_WARNING: + prefix = "WARNING:"; + break; + case LogLevelFlags.LEVEL_MESSAGE: + prefix = "MESSAGE:"; + break; + case LogLevelFlags.LEVEL_INFO: + prefix = "INFO:"; + break; + case LogLevelFlags.LEVEL_DEBUG: + prefix = "DEBUG:"; + break; + default: + prefix = "LOG:"; + break; + } + + log_file.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message); + if (debug_enabled) + stderr.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message); + } + + private void on_uevent (GUdev.Client client, string action, GUdev.Device device) + { + scanner.redetect (); + } + + private static void fix_pdf (string filename) throws Error + { + uint8[] data; + FileUtils.get_data (filename, out data); + + var fixed_file = FileStream.open (filename + ".fixed", "w"); + + var offset = 0; + var line_number = 0; + var xref_offset = 0; + var xref_line = -1; + var startxref_line = -1; + var fixed_size = -1; + var line = new StringBuilder (); + while (offset < data.length) + { + var end_offset = offset; + line.assign (""); + while (end_offset < data.length) + { + var c = data[end_offset]; + line.append_c ((char) c); + end_offset++; + if (c == '\n') + break; + } + + if (line.str == "startxref\n") + startxref_line = line_number; + + if (line.str == "xref\n") + xref_line = line_number; + + /* Fix PDF header and binary comment */ + if (line_number < 2 && line.str.has_prefix ("%%")) + { + xref_offset--; + fixed_file.printf ("%s", line.str.substring (1)); + } + + /* Fix xref subsection count */ + else if (line_number == xref_line + 1 && line.str.has_prefix ("1 ")) + { + fixed_size = int.parse (line.str.substring (2)) + 1; + fixed_file.printf ("0 %d\n", fixed_size); + fixed_file.printf ("0000000000 65535 f \n"); + } + + /* Fix xref format */ + else if (line_number > xref_line && line.str.has_suffix (" 0000 n\n")) + fixed_file.printf ("%010d 00000 n \n", int.parse (line.str) + xref_offset); + + /* Fix xref offset */ + else if (startxref_line > 0 && line_number == startxref_line + 1) + fixed_file.printf ("%d\n".printf (int.parse (line.str) + xref_offset)); + + else if (fixed_size > 0 && line.str.has_prefix ("/Size ")) + fixed_file.printf ("/Size %d\n".printf (fixed_size)); + + /* Fix EOF marker */ + else if (line_number == startxref_line + 2 && line.str.has_prefix ("%%%%")) + fixed_file.printf ("%s", line.str.substring (2)); + + else + for (var i = offset; i < end_offset; i++) + fixed_file.putc ((char) data[i]); + + line_number++; + offset = end_offset; + } + + if (FileUtils.rename (filename, filename + "~") >= 0) + FileUtils.rename (filename + ".fixed", filename); + } + + public static int main (string[] args) + { + Intl.setlocale (LocaleCategory.ALL, ""); + Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALE_DIR); + Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + Intl.textdomain (GETTEXT_PACKAGE); + + Gtk.init (ref args); + + var c = new OptionContext (/* Arguments and description for --help text */ + _("[DEVICE...] - Scanning utility")); + c.add_main_entries (options, GETTEXT_PACKAGE); + c.add_group (Gtk.get_option_group (true)); + try + { + c.parse (ref args); + } + catch (Error e) + { + stderr.printf ("%s\n", e.message); + stderr.printf (/* Text printed out when an unknown command-line argument provided */ + _("Run '%s --help' to see a full list of available command line options."), args[0]); + stderr.printf ("\n"); + return Posix.EXIT_FAILURE; + } + if (show_version) + { + /* Note, not translated so can be easily parsed */ + stderr.printf ("simple-scan %s\n", VERSION); + return Posix.EXIT_SUCCESS; + } + if (fix_pdf_filename != null) + { + try + { + fix_pdf (fix_pdf_filename); + for (var i = 1; i < args.length; i++) + fix_pdf (args[i]); + } + catch (Error e) + { + stderr.printf ("Error fixing PDF file: %s", e.message); + return Posix.EXIT_FAILURE; + } + return Posix.EXIT_SUCCESS; + } + + ScanDevice? device = null; + if (args.length > 1) + { + device = new ScanDevice (); + device.name = args[1]; + device.label = args[1]; + } + + /* Log to a file */ + log_timer = new Timer (); + var path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", null); + DirUtils.create_with_parents (path, 0700); + path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "simple-scan.log", null); + log_file = FileStream.open (path, "w"); + Log.set_default_handler (log_cb); + + debug ("Starting Simple Scan %s, PID=%i", VERSION, Posix.getpid ()); + + var app = new SimpleScan (device); + return app.run (); + } +} |