| 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) |