| Line | Revision | Contents |
| 1 | 53 | #!/usr/bin/python -tt |
| 2 | """ image-process.py (c) 2008 Matthew John Ernisse <mernisse@ub3rgeek.net> |
|
| 3 | ||
| 4 | Redistribution and use in source and binary forms, |
|
| 5 | with or without modification, are permitted provided |
|
| 6 | that the following conditions are met: |
|
| 7 | ||
| 8 | * Redistributions of source code must retain the |
|
| 9 | above copyright notice, this list of conditions |
|
| 10 | and the following disclaimer. |
|
| 11 | * Redistributions in binary form must reproduce |
|
| 12 | the above copyright notice, this list of conditions |
|
| 13 | and the following disclaimer in the documentation |
|
| 14 | and/or other materials provided with the distribution. |
|
| 15 | ||
| 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
| 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
| 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
|
| 19 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
|
| 20 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
|
| 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
|
| 22 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS |
|
| 23 | OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
|
| 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR |
|
| 25 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE |
|
| 26 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
| 27 | ||
| 28 | 59 | $Id: image-process.py,v 1.3 2008/12/31 06:30:48 mernisse Exp $ |
| 29 | 53 | """ |
| 30 | ||
| 31 | 56 | import datetime |
| 32 | 59 | import gio |
| 33 | 53 | import getopt |
| 34 | import os |
|
| 35 | import pyexiv2 |
|
| 36 | 59 | import random |
| 37 | 53 | import re |
| 38 | import sys |
|
| 39 | ||
| 40 | 56 | from stat import * |
| 41 | ||
| 42 | 59 | # directories... I really wish there was a better way to query this crap. |
| 43 | # I should be able to get this from Gnome/XDG |
|
| 44 | 74 | PHOTOS = "%s/%s/mine" % (os.environ["HOME"], "Pictures") |
| 45 | OTHER = "%s/%s/other" % (os.environ["HOME"], "Pictures") |
|
| 46 | 53 | |
| 47 | # these are evil tags that fuck my term up. |
|
| 48 | BLACKLIST = [ |
|
| 49 | "Exif.Nikon3.DataDump", |
|
| 50 | ] |
|
| 51 | ||
| 52 | def Usage(): |
|
| 53 | binary = os.path.basename(sys.argv[0]) |
|
| 54 | print """Usage: %s [-v] FILE ... |
|
| 55 | or: %s -d [-v] DIRECTORY ... |
|
| 56 | ||
| 57 | Process images listed on the command line. |
|
| 58 | Options: |
|
| 59 | -d treat command line argumetnts as directories. |
|
| 60 | 59 | -v display EXIF data prior to renaming |
| 61 | 53 | """ % (binary, binary) |
| 62 | return |
|
| 63 | ||
| 64 | def fileCopy(src, dst): |
|
| 65 | fd = [None, None] |
|
| 66 | buf = None |
|
| 67 | bs = 16384 |
|
| 68 | count = 0 |
|
| 69 | ||
| 70 | try: |
|
| 71 | fd[0] = open(src, "rb") |
|
| 72 | except IOError, e: |
|
| 73 | print "couldn't open src %s (%s)" % (src, str(e)) |
|
| 74 | return 127 |
|
| 75 | ||
| 76 | try: |
|
| 77 | fd[1] = open(dst, "wb") |
|
| 78 | except IOError, e: |
|
| 79 | print "couldn't open dst %s (%s)" % (dst, str(e)) |
|
| 80 | return 127 |
|
| 81 | ||
| 82 | (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, |
|
| 83 | st_mtime, st_ctime) = os.stat(src) |
|
| 84 | ||
| 85 | sys.stdout.write("0%")
|
|
| 86 | ||
| 87 | while True: |
|
| 88 | buf = fd[0].read(bs) |
|
| 89 | if buf: |
|
| 90 | fd[1].write(buf) |
|
| 91 | count += 1 |
|
| 92 | ||
| 93 | pct = (st_size - (count * bs)) / 100 |
|
| 94 | if pct > 25 and pct < 50: |
|
| 95 | sys.stdout.write("..25%")
|
|
| 96 | if pct > 50 and pct < 75: |
|
| 97 | sys.stdout.write("..50%")
|
|
| 98 | if pct > 75 and pct < 100: |
|
| 99 | sys.stdout.write("..75%")
|
|
| 100 | else: |
|
| 101 | break |
|
| 102 | ||
| 103 | fd[0].close() |
|
| 104 | fd[1].flush() |
|
| 105 | fd[1].close() |
|
| 106 | ||
| 107 | try: |
|
| 108 | os.utime(dst, (st_atime, st_mtime)) |
|
| 109 | os.chmod(dst, st_mode) |
|
| 110 | except Exception, e: |
|
| 111 | print "couldn't update dst's attributes. %s" % (str(e)) |
|
| 112 | os.unlink(dst) |
|
| 113 | ||
| 114 | sys.stdout.write("..100%\n")
|
|
| 115 | return 0 |
|
| 116 | ||
| 117 | def printExif(file): |
|
| 118 | global BLACKLIST |
|
| 119 | ||
| 120 | image = pyexiv2.Image(file) |
|
| 121 | image.readMetadata() |
|
| 122 | ||
| 123 | for key in image.exifKeys(): |
|
| 124 | if key in BLACKLIST: |
|
| 125 | continue |
|
| 126 | ||
| 127 | print "%s: %s" % (key, image[key]) |
|
| 128 | ||
| 129 | def dateTimeRename(file): |
|
| 130 | 59 | global PHOTOS |
| 131 | 53 | image = pyexiv2.Image(file) |
| 132 | image.readMetadata() |
|
| 133 | ||
| 134 | make = "UNKN" |
|
| 135 | model = "0000" |
|
| 136 | ||
| 137 | try: |
|
| 138 | capture_date = image['Exif.Photo.DateTimeOriginal'] |
|
| 139 | except: |
|
| 140 | print "couldn't get EXIF information for %s." % (file) |
|
| 141 | 56 | mtime = os.stat(file)[ST_MTIME] |
| 142 | capture_date = datetime.date.fromtimestamp(mtime) |
|
| 143 | pass |
|
| 144 | 53 | |
| 145 | try: |
|
| 146 | make = image['Exif.Image.Make'] |
|
| 147 | model = image['Exif.Image.Model'] |
|
| 148 | except: |
|
| 149 | # this isn't fatal. |
|
| 150 | pass |
|
| 151 | ||
| 152 | fname = "%s_%s-%s.jpg" % ( |
|
| 153 | capture_date.strftime("%Y-%m-%d_%H-%M-%S"),
|
|
| 154 | make, |
|
| 155 | model |
|
| 156 | ) |
|
| 157 | ||
| 158 | archive = "%s/%s" % ( |
|
| 159 | capture_date.strftime("%Y"),
|
|
| 160 | capture_date.strftime("%m_%B")
|
|
| 161 | ) |
|
| 162 | ||
| 163 | 59 | if not os.path.exists("%s/%s" % (PHOTOS, archive)):
|
| 164 | 53 | try: |
| 165 | 59 | os.makedirs("%s/%s" % (PHOTOS, archive))
|
| 166 | 53 | except Exception, e: |
| 167 | print "couldn't make %s/%s, %s" % ( |
|
| 168 | 59 | PHOTOS, |
| 169 | 53 | archive, |
| 170 | str(e) |
|
| 171 | ) |
|
| 172 | return 127 |
|
| 173 | ||
| 174 | 59 | print "%s" % ( fname ) |
| 175 | prefix = fname |
|
| 176 | while True: |
|
| 177 | if os.path.exists("%s/%s/%s" % ( PHOTOS, archive, fname )):
|
|
| 178 | # The dest file exists, let us err on the side of |
|
| 179 | # safety -- try to find ourselves a fname that |
|
| 180 | # exists. |
|
| 181 | fname = prefix + "_" + random.randrange(0,9999) |
|
| 182 | else: |
|
| 183 | break |
|
| 184 | ||
| 185 | fileCopy(file, "%s/%s/%s" % ( PHOTOS, archive, fname )) |
|
| 186 | 53 | return 0 |
| 187 | ||
| 188 | def Main(): |
|
| 189 | 59 | global PHOTOS, OTHER |
| 190 | 53 | |
| 191 | 59 | if not os.path.exists(PHOTOS): |
| 192 | print "%s: does not exist." % (PHOTOS) |
|
| 193 | 53 | sys.exit(127) |
| 194 | ||
| 195 | 59 | if not os.path.exists(OTHER): |
| 196 | print "%s: does not exist." % (OTHER) |
|
| 197 | 56 | sys.exit(127) |
| 198 | ||
| 199 | 53 | dirs = None |
| 200 | 56 | links = None |
| 201 | 53 | verbose = None |
| 202 | 56 | files = [] |
| 203 | 53 | |
| 204 | try: |
|
| 205 | opts, args = getopt.getopt(sys.argv[1:], "dv") |
|
| 206 | except Exception, e: |
|
| 207 | print "str(e)" |
|
| 208 | Usage() |
|
| 209 | return 1 |
|
| 210 | ||
| 211 | for o, a in opts: |
|
| 212 | if o == "-v": |
|
| 213 | verbose = True |
|
| 214 | elif o == "-d": |
|
| 215 | dirs = True |
|
| 216 | else: |
|
| 217 | Usage() |
|
| 218 | return 1 |
|
| 219 | ||
| 220 | 56 | # process the args as dirnames, walk them and push the files up into |
| 221 | # the files list. else if dirs is false, then just copy args [] to |
|
| 222 | # files [] |
|
| 223 | 53 | if dirs: |
| 224 | 56 | for dir in args: |
| 225 | for d in os.walk(dir): |
|
| 226 | for file in d[2]: |
|
| 227 | files.append("%s/%s" % ( d[0], file ))
|
|
| 228 | else: |
|
| 229 | files = args |
|
| 230 | ||
| 231 | for file in files: |
|
| 232 | 59 | if re.search(r'^\.', os.path.basename(file)): |
| 233 | # We don't touch hidden files or folders. |
|
| 234 | continue |
|
| 235 | ||
| 236 | 53 | if not re.search(r'\.jpg$', file, re.I): |
| 237 | 59 | print "%s: moving non-jpeg" % (file) |
| 238 | code = fileCopy(file, "%s/%s" % ( |
|
| 239 | OTHER, |
|
| 240 | os.path.basename(file) |
|
| 241 | ) |
|
| 242 | ) |
|
| 243 | ||
| 244 | if code > 0: |
|
| 245 | return code |
|
| 246 | ||
| 247 | gF = gio.File(file) |
|
| 248 | if not gF.trash(): |
|
| 249 | print "Could not throw out %s" % (file) |
|
| 250 | ||
| 251 | 53 | continue |
| 252 | ||
| 253 | if not os.path.exists(file): |
|
| 254 | 59 | print "%s: File does not exist" % (file) |
| 255 | 53 | continue |
| 256 | ||
| 257 | if verbose: |
|
| 258 | code = printExif(file) |
|
| 259 | 59 | if code > 0: |
| 260 | return code |
|
| 261 | 53 | |
| 262 | 59 | code = dateTimeRename(file) |
| 263 | 53 | if code > 0: |
| 264 | return code |
|
| 265 | ||
| 266 | 59 | # use Gnome's GIO bindings to throw the file |
| 267 | # out, using the Gnome Trash, this seems nicer |
|
| 268 | # than just calling os.unlink |
|
| 269 | gF = gio.File(file) |
|
| 270 | if not gF.trash(): |
|
| 271 | print "Could not throw out %s" % (file) |
|
| 272 | ||
| 273 | ||
| 274 | 53 | if __name__ == "__main__": |
| 275 | if not len(sys.argv) > 1: |
|
| 276 | Usage() |
|
| 277 | sys.exit(1) |
|
| 278 | ||
| 279 | sys.exit(Main()) |
Loggerhead 1.17 is a web-based interface for Bazaar branches