summaryrefslogtreecommitdiff
path: root/trunk/dimbola/copier.py
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/dimbola/copier.py')
-rw-r--r--trunk/dimbola/copier.py236
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
+