1
#! /usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# vim: set expandtab tabstop=4 shiftwidth=4 :
4
5
"""
6
Convert pictures and videos from a directory to another directory
7
where the pictures are recompressed and resized and the videos are
8
converted to FLV, to be displayed by my photos indexer (based on
9
PrettyPhoto).
10
11
Copyright (C) 2008 Aurelien Bompard <aurelien@bompard.org>
12
13
This program is free software: you can redistribute it and/or modify
14
it under the terms of the GNU Affero General Public License as
15
published by the Free Software Foundation, either version 3 of the
16
License, or (at your option) any later version.
17
18
This program is distributed in the hope that it will be useful,
19
but WITHOUT ANY WARRANTY; without even the implied warranty of
20
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21
GNU Affero General Public License for more details.
22
23
You should have received a copy of the GNU Affero General Public License
24
along with this program.  If not, see <http://www.gnu.org/licenses/>.
25
"""
26
27
28
import sys, os, optparse, shutil, commands, tempfile, re, stat, locale, datetime
29
30
thumb_dir = "_thumbs"
31
thumb_size = 90
32
img_size = {"width": 800, "height": 600}
33
video_thumbs = { "top": "./images/video-top.gif", 
34
                 "bottom": "./images/video-bottom.gif",
35
                 "height": 32,
36
                 "width": 1290 }
37
38
use_pil = False # PIL can't preserve EXIF data yet (http://mail.python.org/pipermail/image-sig/2008-January/004787.html)
39
try:
40
    import PIL.Image
41
except ImportError:
42
    use_pil = False
43
44
45
def processDir(dir, options):
46
    """ Go through all directories specified on the command line, and convert them to the dest dir """
47
    dir_clean = getCleanName(dir)
48
    final_dir = os.path.join(options.dest, dir_clean)
49
    # Copy the original dir to the temp dir (if not yet done)
50
    if os.path.exists(final_dir):
51
        try:
52
            print "The final directory already exists:", final_dir
53
            print "Should I go on anyway (no file will be overwritten) ? [Y/n]"
54
            rep = raw_input()
55
        except KeyboardInterrupt:
56
            print "Aborted."
57
            sys.exit(1)
58
        if rep.strip() == "n":
59
            print "Skipped %s !" % dir
60
            return
61
    else:
62
        os.makedirs(final_dir)
63
        for f in os.listdir(dir):
64
            if os.path.isdir(os.path.join(dir,f)):
65
                continue
66
            shutil.copy(os.path.join(dir,f), 
67
                        os.path.join(final_dir,f))
68
            os.chmod(os.path.join(final_dir,f), 
69
                     stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) # chmod 644
70
    # Rename files if needed
71
    if getattr(options, "prefix", None) is not None:
72
        renameFilesInDir(final_dir, options)
73
    # Prepare metadata
74
    if not os.path.exists(os.path.join(final_dir, "_infos.ini")):
75
        infofile = open(os.path.join(final_dir, "_infos.ini"), "w")
76
        dir_name = os.path.basename(dir.rstrip("/"))
77
        # I usually name my photo dirs this way : "2008-06-16 Joe's birthday"
78
        # Pre-fill the metadata if that's the format we find
79
        match = re.match('(20\d\d-\d\d-\d\d) (.*)', dir_name)
80
        if match:
81
            title = match.group(2)
82
            date = match.group(1)
83
            date = datetime.date(int(date[0:4]),int(date[5:7]),int(date[8:10]))
84
            date = date.strftime("%A %d %B %Y").capitalize()
85
        else:
86
            title = dir_name
87
            date = ""
88
        infofile.write("[general]\ntitle = %s\ndate = %s\n" % (title,date))
89
        infofile.close()
90
    files = os.listdir(final_dir)
91
    files.sort()
92
    print "Recompressing images and building thumbnails from %s..." % dir
93
    inc=0
94
    for file in files:
95
        inc += 1
96
        if file.lower().startswith("_") or os.path.isdir(os.path.join(final_dir,file)):
97
            continue # That's either metadata or thumbnails dir
98
        processFile(file, final_dir, options)
99
        if not options.verbose: # Nice progress counter
100
            sys.stdout.write("\r")
101
            sys.stdout.write("[%s/%s]"%(inc,len(files)))
102
            sys.stdout.flush()
103
    if not options.verbose:
104
        print
105
106
def processFile(file, final_dir, options):
107
    """ Handle each picture or movie """
108
    if file.lower().endswith(".flv"):
109
        return # already converted
110
    if file.lower().endswith(".avi") or file.lower().endswith(".wmv") or file.lower().endswith(".mov"):
111
        return processVideo(file, final_dir, options)
112
    if not ( file.lower().endswith(".jpg") or file.lower().endswith(".jpeg") 
113
             or file.lower().endswith(".gif") or file.lower().endswith(".png") ):
114
        return # Unknown file format
115
    filepath = os.path.join(final_dir, file)
116
    # Portrait or landscape format ?
117
    portrait = False
118
    width, height = getSize(filepath)
119
    if height > width:
120
        portrait = True
121
    # Recompress
122
    recompress(filepath, width, height)
123
    if options.verbose:
124
        print "Recompressed %s" % file
125
    # Prepare thumbnails dir
126
    if not os.path.exists(os.path.join(final_dir, thumb_dir)):
127
        os.mkdir(os.path.join(final_dir, thumb_dir))
128
    # Build thumbnail
129
    thumb_filename = os.path.join(final_dir, thumb_dir, file)
130
    if os.path.exists(thumb_filename):
131
        return
132
    if use_pil:
133
        im = PIL.Image.open(filepath)
134
        if portrait: # resize by height
135
            new_height = thumb_size
136
            new_width = int(im.size[0] * thumb_size / float(im.size[1])) # keep proportion
137
        else: # resize by width
138
            new_width = thumb_size
139
            new_height = int(im.size[1] * thumb_size / float(im.size[0])) # keep proportion
140
        im.thumbnail((new_width,new_height), PIL.Image.ANTIALIAS)
141
        im.save(thumb_filename)
142
    else:
143
        size = thumb_size
144
        if portrait:
145
            size = "x%s" % thumb_size
146
        command = """convert -geometry %s "%s" "%s" """ % (size, filepath, thumb_filename)
147
        os.system(command)
148
    if options.verbose:
149
        print "Built thumbnails for %s..." % file
150
151
def processVideo(file, final_dir, options):
152
    """ Handle movies """
153
    filepath = os.path.join(final_dir, file)
154
    thumb_filename = os.path.join(final_dir, thumb_dir, file[:-4]+".jpg")
155
    # Find size
156
    command = """ffmpeg -i "%s" 2>&1 | awk '/Video: / {print $6}'""" % filepath
157
    output = commands.getoutput(command).rstrip(",")
158
    if output:
159
        width = int(output.split("x")[0])
160
        height = int(output.split("x")[1])
161
    else:
162
        width = 640
163
        height = 480
164
    # Prepare thumbnails dir
165
    if not os.path.exists(os.path.join(final_dir, thumb_dir)):
166
        os.mkdir(os.path.join(final_dir, thumb_dir))
167
    # Make thumbnail
168
    if not os.path.exists(thumb_filename):
169
        tmpfd, tmpfile = tempfile.mkstemp(".jpg")
170
        os.close(tmpfd)
171
        command= """ffmpeg -vframes 1 -i "%s" -ss 00:00:01.000 -y -f mjpeg "%s" &>/dev/null""" % (filepath, tmpfile) 
172
        os.system(command)
173
        if use_pil:
174
            im = PIL.Image.open(tmpfile)
175
            im_top = PIL.Image.open(video_thumbs["top"])
176
            im_bottom = PIL.Image.open(video_thumbs["bottom"])
177
            im.paste(im_top, (0,0))
178
            im.paste(im_bottom, (0,height - video_thumbs["height"]) )
179
            im.thumbnail(thumb_size, PIL.Image.ANTIALIAS)
180
            im.save(thumb_filename)
181
        else:
182
            command = """convert -draw 'image Over 0,0 0,0 "%s"' -draw 'image Over 0,%s 0,0 "%s"' -geometry %s "%s" "%s" """ % \
183
                         (video_thumbs["top"], height - video_thumbs["height"], video_thumbs["bottom"], thumb_size,
184
                          tmpfile, thumb_filename)
185
            os.system(command)
186
        os.remove(tmpfile)
187
        if options.verbose:
188
            print "Built thumbnails for %s..." % file
189
    # Convert to FLV (open format, readable by gnash)
190
    if not os.path.exists(filepath[:-4]+".flv"):
191
        tmpfd, tmpfile = tempfile.mkstemp(".flv")
192
        os.close(tmpfd)
193
        command= """ffmpeg -y -i "%s" "%s" &>/dev/null""" % (filepath, tmpfile) 
194
        os.system(command)
195
        os.chmod(tmpfile, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IROTH) # chmod 644
196
        shutil.move(tmpfile, filepath[:-4]+".flv")
197
        if options.verbose:
198
            print "Converted %s to FLV..." % file
199
    os.remove(filepath)
200
    # Specify the size in the metadata (needed by shadowbox, so obsolete now)
201
    #infofile = open(os.path.join(final_dir, "_infos.ini"), "a")
202
    #infofile.write("[%s]\nwidth = %s\nheight = %s\n"%(file[:-4]+".flv",width,height))
203
    #infofile.close()
204
205
def getSize(file):
206
    """ Find the size of a picture using PIL or ImageMagick """
207
    if use_pil:
208
        im = PIL.Image.open(file)
209
        return im.size
210
    else:
211
        output = commands.getoutput('identify "%s"' % file)
212
        if file.lower().endswith(".jpg") or file.lower().endswith(".jpeg"):
213
            properties = output.split(" JPEG ")[1]
214
        elif file.lower().endswith(".gif"):
215
            properties = output.split(" GIF ")[1]
216
        elif file.lower().endswith(".png"):
217
            properties = output.split(" PNG ")[1]
218
        else:
219
            print "Can't identify %s !"%file
220
            sys.exit(1)
221
        size = properties.split(" ")[0].split("x")
222
        return int(size[0]), int(size[1])
223
224
def recompress(file, width, height):
225
    """ Resize pictures and set quality using PIL or ImageMagick """
226
    if use_pil:
227
        im = PIL.Image.open(file)
228
        outim = im
229
        new_width = width
230
        new_height = height
231
        if height > img_size["height"]:
232
            new_height = img_size["height"]
233
            new_width = int(width * img_size["height"] / float(height)) # keep proportion
234
        elif width > img_size["width"]:
235
            new_width = img_size["width"]
236
            new_height = int(height * img_size["width"] / float(width)) # keep proportion
237
        outim = im.resize((new_width, new_height), PIL.Image.ANTIALIAS)
238
        outim.save(file, quality=75)
239
    else:
240
        if height > img_size["height"]:
241
            os.system("""convert -resize x%s "%s" "/tmp/%s" """ % (img_size["height"], file, os.path.basename(file)) )
242
            shutil.move("/tmp/%s"%os.path.basename(file), file)
243
        elif width > img_size["width"]:
244
            os.system("""convert -resize %s "%s" "/tmp/%s" """ % (img_size["width"], file, os.path.basename(file)) )
245
            shutil.move("/tmp/%s"%os.path.basename(file), file)
246
        os.system("""convert -quality 75 "%s" "/tmp/%s" """ % (file, os.path.basename(file)))
247
        shutil.move("/tmp/%s"%os.path.basename(file), file)
248
249
def getCleanName(dirname):
250
    """ Replace non-ascii chars by their equivalent """
251
    dir_clean = dirname.replace(" ", "_").replace("'", "").replace('"', '')
252
    dir_clean = dir_clean.lower()
253
    # dirty, but efficient ! :)
254
    mapping = { "é":"e", "è":"e", "ê":"e", "ç":"c", "à":"a", "â":"a", "ô":"o", "î":"i", "ï":"i", }
255
    for key in mapping:
256
        dir_clean = dir_clean.replace(key, mapping[key])
257
    dir_clean = dir_clean.decode("utf-8").encode("ascii", "ignore")
258
    return dir_clean
259
260
def renameFilesInDir(dir, options) :
261
    """Expects the name of the directory containing the files and
262
       the prefix to add to each new filename"""
263
    liste = os.listdir(dir)
264
    liste.sort()
265
    nombre = len(liste)
266
    inc = 1
267
    print "Renaming files..."
268
    for fichier in liste:
269
        if os.path.isdir(os.path.join(dir,fichier)):
270
            if options.verbose:
271
                print "Skipping dir %s" % fichier
272
            continue
273
        # Extract extention
274
        dot = fichier.rfind(".")
275
        if dot == -1 :
276
            print "ERROR: I can't extract the extention for file %s" % fichier
277
            sys.exit(1)
278
        ext = fichier[dot:].lower()
279
        if ext not in [".jpg", ".jpeg", ".mov", ".avi", ".flv", ".wmv", ".mov"]:
280
            if options.verbose:
281
                print "Ignoring %s" % fichier
282
            continue
283
        # fill the index with enough zeros on the left
284
        incLenght = len(str(nombre))
285
        incStr = str(inc).zfill(incLenght)
286
        newfile = os.path.join(dir, options.prefix+"_"+incStr+ext)
287
        oldfile = os.path.join(dir, fichier)
288
        # while the destination file exists, find another number
289
        while os.path.exists(newfile):
290
            if options.verbose:
291
                print "The file", newfile, "already exists !"
292
            inc += 1
293
            incStr = str(inc).zfill(incLenght)
294
            newfile = os.path.join(dir, options.prefix+'_'+incStr+ext)
295
            if options.verbose:
296
                print "Trying", newfile+"..."
297
        if options.verbose:
298
            print "Renaming", os.path.basename(oldfile), " --> ", os.path.basename(newfile)
299
        else:
300
            sys.stdout.write(".")
301
            sys.stdout.flush()
302
        os.rename(oldfile, newfile)
303
        inc += 1
304
    if not options.verbose:
305
        print
306
307
def parseOpts():
308
    usage = "usage: %prog [options] original-folder [original-folder-2 ...]"
309
    parser = optparse.OptionParser(usage)
310
    parser.add_option("-p", "--prefix", dest="prefix",
311
                      help="prefix filenames with this PREFIX", metavar="PREFIX")
312
    parser.add_option("-d", "--destination", dest="dest",
313
                      help="the resulting files will be placed in this folder")
314
    parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False)
315
316
    (options, args) = parser.parse_args()
317
    if len(args) == 0:
318
        parser.error("must specify a directory to process")
319
    if not options.dest:
320
        options.dest = "/tmp"
321
    for dir in args:
322
        if not os.path.exists(dir) or not os.path.isdir(dir):
323
            parser.error('The directory %s does not exist.' % dir)
324
325
    print
326
    print "Folder(s) to process:", ", ".join(args)
327
    print "Destination folder:", options.dest
328
    # Find subdirs
329
    dirs = args[:] # copy list object
330
    total_files = 0
331
    for dir in dirs:
332
        for base, subdirs, subfiles in os.walk(dir):
333
            if subdirs:
334
                dirs.extend( [os.path.join(base, d) for d in subdirs] )
335
            total_files += len(subfiles)
336
    print "Number of files to be processed:", total_files
337
    if getattr(options, "prefix", None) is not None:
338
        print "Rename files with prefix:", options.prefix
339
    print "Is it OK ? (Ctrl-C to abort)"
340
    raw_input()
341
    return options, dirs, args
342
343
344
#### MAIN ####
345
346
def main(argv) :
347
348
    try:
349
        options, dirs, args = parseOpts()
350
    except KeyboardInterrupt:
351
        print "Aborted."
352
        sys.exit(0)
353
    # set the correct locale for the names of the days and months
354
    locale.setlocale(locale.LC_ALL, '')
355
    dirs.sort()
356
    for dir in dirs:
357
        processDir(dir, options)
358
    # Adapt below to your upload needs
359
    print "To upload, you may run:"
360
    upload_cmd = [ "scp -r" ]
361
    for dir in args:
362
        upload_cmd.append(os.path.join(options.dest, getCleanName(dir)))
363
    upload_cmd.append("your.server:your/destination/dir/")
364
    print " ".join(upload_cmd)
365
        
366
367
368
if __name__ == '__main__' : main(sys.argv)