# Copyright (C) 2009 Lars Wirzenius # # 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 . 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