diff options
Diffstat (limited to 'trunk/dimbola/copier.py')
-rw-r--r-- | trunk/dimbola/copier.py | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/trunk/dimbola/copier.py b/trunk/dimbola/copier.py new file mode 100644 index 0000000..fd9fc95 --- /dev/null +++ b/trunk/dimbola/copier.py @@ -0,0 +1,236 @@ +# Copyright (C) 2009 Lars Wirzenius <liw@liw.fi> +# +# 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. +# +# 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 <http://www.gnu.org/licenses/>. + + +import optparse +import os +import pwd +import re + +import gnomevfs +import pyexiv2 + +import dimbola + + +# The default template for renaming files. +TEMPLATE = "%(date)s/%(username)s-%(date)s-%(counter)s%(suffix)s" + + +class ImageDict: + + """Hold data for templating output filenames. + + The output filename template is just a string suitable for Python + string substitution, with the % operator. The data for the substition + needs to come from a dictionary. Since we want to get data both from + the image meta data (EXIF headers), and elsewhere, we use this class + to collect them all. + + During initialization, we copy all the EXIF headers of the image into + our own dictionary, and then add some more data. + + To do this, the caller must supply the constructor a pathname to the + image, a pyexiv2.Image instance, and a counter. The counter should be + incremented by the caller for each image; this allows the user to + specify a template like "%(year)s-%(counter)s". + + We require the caller to supply the pyexiv2.Image instance so that + we don't need to do any I/O. This means there should be no reason for + this class to fail. + + """ + + def __init__(self, pathname, image, counter): + date = image["Exif.Image.DateTime"] + + self.dict = { + "username": self.get_username(), + "suffix": self.get_input_suffix(pathname), + "cameracounter": self.get_camera_counter(pathname), + "counter": counter, + "date": "%04d-%02d-%02d" % (date.year, date.month, date.day), + "year": date.year, + "month": date.month, + "day": date.day, + "hour": date.hour, + "min": date.minute, + "sec": date.second, + } + + optional_key_prefix = "Exif.Image." + for key in image.exifKeys(): + self.dict[key] = image[key] + if key.startswith(optional_key_prefix): + key2 = key[len(optional_key_prefix):] + self.dict[key2] = image[key] + + def __getitem__(self, key): + return self.dict[key] + + def __contains__(self, key): + return key in self.dict + + def get_input_suffix(self, pathname): + """Return the suffix of the input filename, or empty.""" + dummy, suffix = os.path.splitext(pathname) + return suffix + + def get_camera_counter(self, pathname): + """Return the image counter in the input filename, or empty.""" + basename = os.path.basename(pathname) + basename, ext = os.path.splitext(basename) + m = re.search(r"\d+", basename) + if m: + return m.group() + else: + return "" + + def get_username(self): # pragma: no cover + """Return username of the user.""" + return pwd.getpwuid(os.getuid()).pw_name + + + +class Copier: + + """Copy digital photograps from memory card into desired location. + + We scan the desired location recursively for files that have one + of the desired MIME types. Each image that we find, we copy to the + desired output location. Optionally, we delete the original. + The output filenames may be identical to the basenames of the + originals, or they may be constructed based on a template that + gets filled in with data from the input pathname, EXIF headers, + and other places. + + """ + + known_image_types = set([ + "image/x-canon-cr2", + "image/x-nikon-nef", + "image/jpeg", + ]) + + def __init__(self): + self.counter = 0 + + def is_image_file(self, pathname): # pragma: no cover + """Determine whether a given file is a (supported) image file.""" + uri = gnomevfs.get_uri_from_local_path(os.path.abspath(pathname)) + mime_type = gnomevfs.get_mime_type(uri) + return mime_type in self.known_image_types + + def find_input_files(self, root): # pragma: no cover + """Recursively generate list of input files in a directory tree.""" + all_names = [] + for x, y, names in dimbola.filterabswalk(self.is_image_file, root): + all_names += names + all_names.sort() + return all_names + + def read_exif(self, pathname): # pragma: no cover + """Read the EXIF data from a given file.""" + image = pyexiv2.Image(pathname) + image.readMetadata() + return image + + def output_name(self, input_name, options): + """Return the output name for a given input file.""" + + if options.rename: + image = self.read_exif(input_name) + image_dict = ImageDict(input_name, image, self.counter) + basename = options.template % image_dict + else: + basename = os.path.basename(input_name) + + return os.path.join(options.output, basename) + + def create_option_parser(self): # pragma: no cover + """Create an OptionParser instance for this app.""" + parser = optparse.OptionParser() + parser.add_option("-i", "--input", metavar="DIR", default=".", + help="Scan DIR for files to import. " + "(Default: %default)") + parser.add_option("-o", "--output", metavar="DIR", default=".", + help="Write output to DIR. (Default: %default)") + parser.add_option("-t", "--template", metavar="TEMPLATE", + default=TEMPLATE, + help="Use TEMPLATE when renaming files. " + "(Default: %default)") + parser.add_option("-r", "--rename", action="store_true", + help="Rename files when copying.") + parser.add_option("--move", action="store_true", + help="Move files: delete originals after they " + "have been copied.") + parser.add_option("--verbose", action="store_true", + help="provide some progress output") + return parser + + def parse_command_line(self): # pragma: no cover + """Parse the command line for this app.""" + parser = self.create_option_parser() + options, args = parser.parse_args() + if args: + raise Exception("No non-option command line arguments allows.") + return options + + def copy_file(self, input_name, options): # pragma: no cover + """Copy an input file according to options.""" + while True: + self.counter += 1 + try: + output_name = self.output_name(input_name, options) + except KeyError: + print 'ERROR: exif problem with %s' % input_name + return + except AttributeError: + print 'ERROR: exif problem with %s' % input_name + return + except IOError: + print 'ERROR: exif problem with %s' % input_name + return + if not os.path.exists(output_name): + break + output_dir = os.path.dirname(output_name) or "." + if os.path.exists(options.output) and not os.path.exists(output_dir): + os.makedirs(output_dir) + if options.verbose: + i = self.copied_files + 1 + n = len(self.input_files) + print "%d/%d: %s -> %s" % (i, n, input_name, output_name) + if options.move: + os.rename(input_name, output_name) + else: + dimbola.safe_copy(input_name, output_name, None) + + def find_total_bytes(self, pathnames): # pragma: no cover + """Find the total number of bytes in the given files.""" + return sum([os.stat(x).st_size for x in pathnames]) + + def run(self): # pragma: no cover + """Main program of the application.""" + options = self.parse_command_line() + self.input_files = self.find_input_files(options.input) + self.total_bytes = self.find_total_bytes(self.input_files) + self.copied_files = 0 + self.copied_bytes = 0 + for input_name in self.input_files: + self.this_file_bytes = 0 + self.copy_file(input_name, options) + self.copied_files += 1 + self.copied_bytes += self.this_file_bytes + |