#!/usr/bin/env python

VERSION = '0.0.1'
NAME = 'shellscan.py'

############################ INFO ##########################################
#
# Name:          scan_pages.py
# Version:       0.0.1
# Author:        Matt Kowalczyk
# Contact:       matt.kowalczyk AT gmail.com
# Last Updated:  Tue Mar  2 00:27:18 PST 2010
#
# A wrapper script for the scanimage(1) command.
# 
############################################################################

########################### LEGAL ##########################################
#
# Copyright (c) 2010, Matt Kowalczyk
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. THIS CODE IS FREE FOR PERSONAL USE.  HOWEVER, IF IT IS TO
# BE USED COMMERCIALLY YOU MUST GET PERMISSION FROM THE AUTHOR IN WRITING.
#
# WHEN REDISTRIBUTING OR USING THIS CODE IN YOUR OWN FREE PROGRAMS YOU MUST
# GIVE CREDIT TO THE AUTHOR AND KEEP THIS LEGAL INFORMATION INTACT. USERS ARE
# FREE TO MODIFY AND REDISTRIBUTE THIS CODE SO LONG AS CREDIT IS GIVEN TO THE
# ORIGINAL AUTHOR AND THIS LEGAL INFORMATION IS INCLUDED IN AN UNMODIFIED
# STATE.
#
############################################################################

######################### CHANGE_LOG #######################################
# VERSION 0.0.1:
#   - initial release
#
############################################################################

from optparse import OptionParser
import os
import sys
import StringIO
import math

"""
This script is a wrapper for the scanimage(1) command to scan multiple pages.
I always found it a headache to keep track of the command arguments and this
motivated me to write this script.

The script will also use the convert(1) command to trim any white-space from
the scanned images.

Examples:

Scans two page. The scanned images are named 1.jpg and 2.jpg

	scan_pages.py 2

Scans 5 pages using double sided mode. Double sided mode requires two steps.
The first step will scan the odd numbered pages (1, 3, and 5). The script
will then prompt the user to flip the pages and scan the remaining pages
even pages (2, 4) The images are nameb 1.jpg, 2.jpg, 3.jpg, 4.jpg, and 5.jpg

	scan_pages.py -d 5
"""

DEVICE_NAME="hpaio:/usb/Officejet_Pro_8500_A909a?serial=MY9AP430CN"
"""
The only configurable value indicating the device name. Set to 'None' to
allow the scanimage(1) command to select the scanner
"""

class scan_image_option():
    """
    class wrapper represents a command line argument to scanimage
    """

    LONG_ARGUMENT_SEP  = "="
    """long argument name / value seperator"""

    SHORT_ARGUMENT_SEP = " "
    """short argument name / value seperator"""

    def __init__(self, argument_flag, argument_value = None):
        """
        create a new command line option.

        :param argument_flag: the name of the argument. it should include the
        prefixed dashes. e.g. "--version" or "-v"

        :param argument_value: (optional) value for the command line argument
        """

        self.argument_flag  = argument_flag
        self.argument_value = argument_value

        if self.argument_flag.startswith("--"):
            self.argument_sep = scan_image_option.LONG_ARGUMENT_SEP
        else:
            self.argument_sep = scan_image_option.SHORT_ARGUMENT_SEP

    def clone(self):
        """
        returns a clone of this scan_image_option instance
        """
        rv = scan_image_option(self.argument_flag, self.argument_value)
        return rv

    def get_name(self):
        """
        returns the argument name, excluding the "-" character(s).
        """
        return self.argument_flag.lstrip("-")

    def get_value(self):
        """
        returns the argument value. returns None if the value was undefined
        """
        return self.argument_value

    def __str__(self):
        """
        returns a string representation of the argument
        """

        if self.argument_value is None:
            return "%s" % (self.argument_flag)
        else:
            return "%s%s'%s'" % (self.argument_flag,
                                 self.argument_sep,
                                 self.argument_value)

    def __hash__(self):
        """
        returns a hash for the argument
        """

        return self.argument_flag.__hash__()

class scan_image_command():
    """
    represents a complete scanimage command
    """

    def __init__(self):
        """
        creates a new instance of the scan_image_command. it uses default
        values and will scan a single page.
        """

        """
        the scanimage command
        """
        self.command = "scanimage"
        
        """
        hash of device options -- we don't want duplicates!
        """
        self.options_list = {}

        """
        configure any default options
        """
        self._set_default_options()

        """
        default start page of 1
        """
        self.set_page_start(1)

    def _set_default_options(self):
        """
        configure any default options
        """

        # default to jpeg output
        #
        device_option = scan_image_option("--format", "jpeg")
        self._add_option(device_option)

        # do not compress
        #
        device_option = scan_image_option("--compression", "None")
        self._add_option(device_option)

        # gray scale
        #
        device_option = scan_image_option("--mode", "Gray")
        self._add_option(device_option)

        # resolution of 120 works well for documents
        #
        device_option = scan_image_option("--resolution", "120")
        self._add_option(device_option)

        # the output image format
        #
        device_option = scan_image_option("--batch", "%d.jpg")
        self._add_option(device_option)

        # use batch scanning, even if it's a single page
        #
        device_option = scan_image_option("--batch-scan", "yes")
        self._add_option(device_option)

    def _add_option(self, device_option):
        """
        add a command option specified by device_option. device_option should
        be of type scan_image_option.
        
        :param device_option: the device option
        """
        self.options_list[device_option] = device_option

    def set_device(self, device_name):
        """
        set the name of the scanning device

        :param device_name: the name of the device
        """
        device_option = scan_image_option("-d", device_name)
        self._add_option(device_option)

    def set_double(self):
        """
        scan in double sided mode
        """

        device_option = scan_image_option("--batch-double")
        self._add_option(device_option)

    def set_page_count(self, num_pages):
        """
        set the number of pages to scan

        :param num_pages: the number of pages to scan
        """
        device_option = scan_image_option("--batch-count", num_pages.__str__())
        self._add_option(device_option)

    def get_page_count(self):
        """
        returns the number of pages to scan
        """
        bc =  self.options_list["batch-count"]
        if bc is None:
            raise Exception("batch-count not found")

        return int(bc.get_value())

    def get_page_start(self):
        bc =  self.options_list["batch-start"]
        if bc is None:
            raise Exception("batch-count not found")

        return int(bc.get_value())

    def set_page_start(self, page):
        """
        set the page at which to begin scanning

        :param page: the page number to start the scan
        """

        device_option = scan_image_option("--batch-start", page.__str__())
        self._add_option(device_option)

    def format_command(self):
        """
        returns a string representing the formatted the command
        """

        sb = StringIO.StringIO()

        sb.write(self.command)
        sb.write(" ")


        for device_option in self.options_list.values():
            sb.write(device_option.__str__())
            sb.write(" ")

        return sb.getvalue()

    def execute_command(self):
        """
        executes the command
        """
        os.system(self.format_command())

    def clone(self):
        """
        returns a deep-clone of the command
        """

        rv = scan_image_command()
        for device_option in self.options_list.values():
            rv._add_option(device_option.clone())
        return rv

    def __str__(self):
        """
        returns a string representation of the command
        """
        return self.format_command()

class scan_image():
    """
    this class represents a scan job. it allows for the ability to scan
    N-number of pages and optionally to scan double-sided page.
    """

    def __init__(self):
        """
        creates a new scan image job
        """

        self.first_pass  = scan_image_command()
        self.second_pass = None
        self.num_pages   = None
        self.double_sided = False

    def set_device(self, device):
        """
        sets the device to scan from

        :param device: the device name
        """

        self.first_pass.set_device(device)

    def set_page_count(self, pages):
        """
        sets the number of pages to scan

        :param pages: the number of pages to scan
        """

        if pages <= 0:
            raise Exception("Invalid page count: %d" % pages)

        self.num_pages = int(pages)
        self.first_pass.set_page_count(pages)

        # updated the double sided scanning options
        #
        if self.double_sided is True: self.set_double()

    def set_double(self):
        """
        double sided scanning mode should be used.
        """

        if self.num_pages <= 1:
            raise Exception("double sided requires 2 or more pages")

        self.double_sided = True
        self.first_pass.set_double()
        self.second_pass = self.first_pass.clone()
        self.second_pass.set_page_start(2)

        first_pass_pages = int(math.ceil(self.num_pages / 2.0))
        secon_pass_pages = self.num_pages - first_pass_pages

        self.first_pass.set_page_count(first_pass_pages)
        self.second_pass.set_page_count(secon_pass_pages)

    def execute(self):
        """
        execute the job. if double sided scanning mode is used the
        user will be prompted to prepare the documents for scanning the
        reverse side.
        """

        print self.first_pass
        self.first_pass.execute_command()

        if self.second_pass is not None:
            raw_input("Press any key to resume scan")
            print self.second_pass
            self.second_pass.execute_command()

def usage():
    """
    Prints the usage clause
    """

    print "%s [-d | --double] [total_pages]" % __file__
    os._exit(1)

def main():
    parser = OptionParser()
    parser.add_option("-d", "--double",
                      action  = "store_true",
                      dest    = "is_double",
                      help    = "scan double",
                      metavar = "IS_DOUBLE",
                      default = "false")

    (options, args) = parser.parse_args()

    if len(args) != 1: usage()

    s = scan_image()
    s.set_device(DEVICE_NAME)
    s.set_page_count(args[0])

    if options.is_double is True: s.set_double()

    s.execute()

    # trim scanned images
    #
    for i in xrange(1, s.num_pages + 1):
        print "convert %d.jpg -trim %d.jpg" % (i, i)
        rv = os.system("convert %d.jpg -trim %d.jpg" % (i, i))
        if rv != 0: break

if __name__ == "__main__":
    main()

