Last active
August 29, 2015 14:06
-
-
Save rob-murray/b5b34a30616390c9f374 to your computer and use it in GitHub Desktop.
Check images in a directory for valid EXIF image creation date and update if necessary.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'optparse' | |
require 'mini_exiftool' | |
# Check images in a directory for valid EXIF image creation date and update if necessary. | |
# Can be used to fix older photos that do not have to the correct EXIF `date_time_original` field data. | |
# | |
# If not present then attempt fallback and update `date_time_original` field | |
# If fallback not available then otherwise prompt for input or use default date. | |
# | |
# Usage: ./update_images.rb -d ./directory/ | |
# | |
DEFAULT_DATE = '2004-10-01 00:00:01 +00:00' | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = 'Update EXIF Date for all images in a directory' | |
opts.separator '' | |
opts.on('-d', '--directory [DIRECTORY]', 'Specify the path to the directory to process') do |directory| | |
options[:directory] = directory | |
end | |
opts.on('-f', '--force', 'Force to be asked to update image date') do |_force| | |
options[:force] = true | |
end | |
opts.on("-h", "--help", "Displays help") do | |
puts opts | |
exit | |
end | |
end.parse! | |
def main(options) | |
successes = 0 | |
failures = [] | |
image_directory = options[:directory].end_with?('/') ? options[:directory] : options[:directory] << '/' | |
images = image_list(image_directory) | |
force = true if options[:force] | |
print "Processing #{images.size} images from #{image_directory}.\n" | |
print "Default date is #{DEFAULT_DATE}.\n" | |
images.each do |image_file| | |
result = process_image(image_file, force) | |
if result | |
successes += 1 | |
else | |
failures << image_file | |
end | |
end | |
print "Processed #{successes} / #{images.size} images\n" | |
print "Failures: #{failures}\n" | |
end | |
def image_list(image_directory) | |
image_matcher = "#{image_directory}*.jpg" | |
Dir.glob(image_matcher, File::FNM_CASEFOLD) # case insensitive glob | |
end | |
def process_image(photo_file, force = false) | |
puts "Working image: #{photo_file}\n" | |
photo = MiniExiftool.new(photo_file) | |
display_metadata(photo) | |
if !force && matched_date = find_ideal_date(photo) | |
display_date(matched_date) | |
print ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" | |
# dont need to do anything | |
return true | |
elsif !force && matched_date = find_date(photo) | |
display_date(matched_date) | |
set_date(photo, matched_date) | |
print ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" | |
return true | |
else | |
print "Unable to find a date for image: #{photo_file}\n" | |
if input_date = get_date_from_user_input | |
set_date(photo, input_date) | |
print ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" | |
return true | |
end | |
print ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" | |
return false | |
end | |
end | |
def display_metadata(p) | |
print "title: #{p.title}\n" | |
print "image_description: #{p.image_description}\n" | |
print "artist: #{p.artist}\n" | |
print "make: #{p.make}\n" | |
print "model: #{p.model}\n" | |
print "software: #{p.software}\n" | |
end | |
def display_date(date) | |
fmt = '%Y-%m-%d %H:%M:%S' | |
print "Matched date: #{date.strftime(fmt)}\n" if date | |
end | |
def find_ideal_date(p) | |
if p.date_time_original && valid_date?(p.date_time_original) | |
print "Matched via date_time_original\n" | |
return p.date_time_original | |
end | |
end | |
def find_date(p) | |
if p.create_date && valid_date?(p.create_date) | |
print "Matched via create_date\n" | |
return p.create_date | |
end | |
if p.modify_date && valid_date?(p.modify_date) | |
print "Matched via modify_date\n" | |
return p.modify_date | |
end | |
if p.metadata_date && valid_date?(p.metadata_date) | |
print "Matched via metadata_date\n" | |
return p.metadata_date | |
end | |
end | |
def set_date(p, date) | |
print "Setting #{date} to #{p.filename}\n" | |
p.date_time_original = date | |
p.save | |
end | |
def valid_date?(date) | |
result = DateTime.parse date.to_s rescue nil | |
!result.nil? | |
end | |
def get_date_from_user_input | |
puts "Enter a complete date or the year? Enter 'def' to take default from script. [d|y|def]\n" | |
input = gets.chomp | |
if input == 'y' | |
ask_for_year | |
elsif input == 'def' | |
use_default_date | |
else | |
ask_for_date | |
end | |
end | |
# 2007-09-23 18:25:39 +0100 | |
def ask_for_date | |
puts "Enter a date for this photo or leave blank to skip. Format. '%Y-%m-%d %H:%M:%S %z' '2001-02-03 04:05:06 +07:00'\n" | |
datetime = gets.chomp | |
DateTime.strptime(datetime, '%Y-%m-%d %H:%M:%S %z') unless datetime.empty? | |
end | |
def ask_for_year | |
puts "Enter a year. Other date components will be filled with defaults\n" | |
year = gets.chomp | |
DateTime.strptime(year, '%Y') unless year.empty? | |
end | |
def use_default_date | |
DateTime.strptime(DEFAULT_DATE, '%Y-%m-%d %H:%M:%S %z') | |
end | |
if __FILE__ == $0 | |
main(options) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment