#!/usr/bin/python # ExiFilm -- Add film exposure metadata to EXIF tags of digital images # Copyright (C) 2009 James E. Blair # # 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 . import pygtk pygtk.require('2.0') import gtk, gobject import gtk.glade import gtk.keysyms from gtk import gdk import os, sys import pyexiv2 import datetime import decimal, fractions import re HALF_STOP_SCALE = ['1', '1.2', '1.4', '1.7', '2', '2.4', '2.8', '3.3', '4', '4.8', '5.6', '6.7', '8', '9.5', '11', '13', '16', '19', '22', '27', '32', '38', '45', '54', '64', '76', '91'] THIRD_STOP_SCALE = ['1', '1.1', '1.2', '1.4', '1.6', '1.8', '2', '2.2', '2.5', '2.8', '3.2', '3.5', '4', '4.5', '5.0', '5.6', '6.3', '7.1', '8', '9', '10', '11', '13', '14', '16', '18', '20', '22', '25', '29', '32', '36', '40', '45', '51', '57', '64', '72', '81', '91'] CAMERA_MOVEMENTS = [ 'rising', 'falling', 'forward tilt', 'backward tilt', 'left shift', 'right shift', 'left swing', 'right swing', ] ID = 'ID' FILM = 'Film' CARRIER = 'Carrier' PROCESS = 'Process' FRONT_MOVEMENTS = 'Front movements' REAR_MOVEMENTS = 'Rear movements' PRIVATE_KEYS = [ ID, FILM, CARRIER, PROCESS, FRONT_MOVEMENTS, REAR_MOVEMENTS, ] ISO_RE = re.compile(r'\d\d+') def to_rational(v): if '/' in v: n,d = v.split('/') else: n,d = (v,1) return pyexiv2.Rational(int(n), int(d)) def from_rational(v): if not v: return v n,d = str(v).split('/') if d == '1': return str(n) return str(v) def from_fstop(v): if '/' in v: whole, frac = v.split(' ') n,d = frac.split('/') if d == '2': scale = HALF_STOP_SCALE if d == '3': scale = THIRD_STOP_SCALE i = scale.index(whole) i += int(n) f = fractions.Fraction.from_decimal(decimal.Decimal(str(scale[i]))) return str(f) f = fractions.Fraction.from_decimal(decimal.Decimal(v)) return str(f) def to_fstop(v): if '/' in v: n,d = v.split('/') return str(float(n)/float(d)) return v def encode_comments(d, comments): ret = '' for k in PRIVATE_KEYS: if k in d.keys(): ret += '%s: %s\n' % (k, d[k]) ret += '\n'+comments return ret def decode_comments(comments): d = {} ret = '' lines = comments.split('\n') while lines: line = lines.pop(0).strip() if not line: break if ':' in line: k,v = line.split(':', 1) d[k.strip()] = v.strip() else: ret += line+'\n' break return d, ret+'\n'.join(lines) class ExiFilm(object): def __init__(self): self.image = None self.xml = gtk.glade.XML('exifilm.glade', 'main_window') self.file_combo = self.xml.get_widget('file') self.id_entry = self.xml.get_widget('id') self.film_entry = self.xml.get_widget('film') self.carrier_entry = self.xml.get_widget('carrier') self.process_entry = self.xml.get_widget('process') self.lens_entry = self.xml.get_widget('lens') self.aperture_entry = self.xml.get_widget('aperture') self.shutter_entry = self.xml.get_widget('shutter') self.front_button = self.xml.get_widget('front') self.rear_button = self.xml.get_widget('rear') self.description_entry = self.xml.get_widget('description') self.date_button = self.xml.get_widget('date') self.comments_text = self.xml.get_widget('comments') self.comments_buffer = gtk.TextBuffer() self.comments_text.set_buffer(self.comments_buffer) # Keep the last selected date to provide a sane default self.lastdate = None dic = { 'on_main_window_destroy': self.window_closed, 'on_main_window_key_press_event': self.key_press, 'on_date_clicked': self.date_clicked, 'on_front_clicked': self.front_clicked, 'on_rear_clicked': self.rear_clicked, 'on_file_changed': self.file_changed, } self.xml.signal_autoconnect (dic) self.file_store = gtk.ListStore(gobject.TYPE_STRING) cell = gtk.CellRendererText() self.file_combo.pack_start(cell, True) self.file_combo.add_attribute(cell, 'text', 0) self.file_combo.set_model(self.file_store) self.filename = None first = None self.directory = '.' if len(sys.argv)>1: self.directory = sys.argv[1] files = os.listdir(self.directory) files.sort() for fn in files: if not (fn.lower().endswith('.jpeg') or fn.lower().endswith('.jpg')): continue if not first: first = fn self.file_store.append((fn,)) if first: self.file_combo.set_active(0) def file_changed(self, widget, data=None): if self.filename: self.save() i = self.file_combo.get_active() fn = self.file_store[i][0] self.load(fn) self.id_entry.grab_focus() def key_press(self, widget, data=None): if data.keyval == gtk.keysyms.Page_Up: i = self.file_combo.get_active() if i>0: self.file_combo.set_active(i-1) return True if data.keyval == gtk.keysyms.Page_Down: i = self.file_combo.get_active() if i1 and sys.argv[1][0]=='-': print "Usage: %s [PATH]" % sys.argv[0] print print " PATH is a directory with JPEG files to edit." print " PATH defaults to the current directory." else: main()