summaryrefslogtreecommitdiff
path: root/trunk/dimbola/copier.py
blob: fd9fc952e4441b5b2f4aa108858b4d8c322169f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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