#! /usr/bin/ruby # :Title: exifDirs.rb: Exif based image sorting. # exifDirs.rb: Exif based image sorting. # Author:: Matt Foster (mailto:mattfoster@clara.co.uk) # Copyright:: Copyright (c) Matt Foster (mailto:mattfoster@clara.co.uk) # Process jpeg images by looking at exif information, # and linking them to directories depending on what we find. # currently only know what to do with dates and orientations, but is easy to # extend. # * Uses a few bits and pieces I haven't used before, like ostruct and optparse. # * Uses a config file to specify what to look at, but currently, operations # functions are hard-coded. # # Changelog: # 20050924: Replaced transform with to_a. Following changes to yaml.rb # 20051022: Fixed the YAML, and changed the default config file. require 'exif' require 'time' require 'fileutils' require 'yaml' require 'ostruct' require 'optparse' # The exifDirs class... all of the operations live here. class ExifDirs def initialize(verbose = false, config = '~/.exifDirs.rc') @config = File.expand_path(config) loadConfig @opts = parseOpts @opts.verbose ||= verbose if @opts.verbose puts "Verbose mode on." puts "Source dir: #{@opts.source}" puts "Destination dir: #{@opts.dest}" end setup process @opts.source end # Create the necessary dirs. def setup @types.each_key do | key | puts "Creating dir: #{@opts.dest}/#{@types[key]}" if @opts.verbose FileUtils.mkdir_p "#{@opts.dest}/#{@types[key]}" end if @types.has_key? 'Orientation' @orientationDirs.each_value do |entry| puts "Creating dir: #{entry}" if @opts.verbose FileUtils.mkdir_p "#{@opts.dest}/#{entry}" end end end # process a given directory def process(dir) puts "Processing: #{dir}." # Give us access to the array outside of the iterator. dest = Array.new Dir.chdir dir # Iterate over JPEGS. Dir["**/*.jpg"].each do |file| unless Dir.pwd =~ /#{@opts.dest}(\/)*.*/ exif = Exif.new file unless exif.list_tags(Exif::IFD::EXIF) == nil dest = [] puts "Processing: #{file}" if @opts.verbose # Get the destination dir. @types.each_key do |key| op = "self.#{@types[key]}Op(\"#{exif[key]}\")" dest.push(eval(op)) end # types # Now, process what we've learned. makeLinks Dir.pwd, file, dest end end end puts "Examined #{Dir['**/*.jpg'].length} files." exit 0 end def undef_method? return nil end # Turn orientation into a path name. def orientationOp(exifKey) return "#{@opts.dest}/#{@orientationDirs[exifKey]}" end # Decodes dates, and returns a corresponding path def dateOp(exifKey) unless exifKey == nil date = Time.parse exifKey.gsub(':', '') dir = date.strftime "#{@opts.dest}/date/%Y%m%d/" else puts "Error parsing date." if @opts.verbose return nil end return dir end # Create a filename with and inbuilt suffix.. I.e convert from # foo.jpg to foo-1.jpg using the first available integer. def addSuffix(path) dir, base = File.split(path) name = File.basename base, '.jpg' num = 1 nPath = "#{dir}/#{name}-#{num}.jpg" if File.exist? nPath num += 1 nPath = "#{dir}/#{name}-#{num}.jpg" end nPath end # Create symlinks def makeLinks(dir, source, dest) dest.each do |dst| unless dst == nil if ! File.exist? dst FileUtils.mkdir_p dst end end sourcePath = "#{dir}/#{source}".gsub("//", "/") destPath = "#{dst}/#{File.basename(source)}".gsub("//", "/") begin if File.exist? destPath # Assume it's because the cameras number was reset. destPath = addDestSuffix(destPath) end if File.exist? sourcePath puts "Linking:\n\t#{sourcePath} to\n\t#{destPath}" if @opts.verbose FileUtils.ln_s sourcePath, destPath else puts "Link not created." end rescue puts "Error, couldn't link #{sourcePath} to #{destPath}." end end end # Load the YAML config, creating a default one if it doesn't exist. def loadConfig if File.exist? @config yaml_tree = YAML::parse_file @config @types = yaml_tree.select('/types')[0].transform @orientationDirs = yaml_tree.select('/orientations')[0].transform else puts "Config file #{@config} doesn't exist. Creating default config." config = File.new(@config, "w") config.write <