Skip to content

Instantly share code, notes, and snippets.

@JamesHarrison
Created June 7, 2012 21:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamesHarrison/2891708 to your computer and use it in GitHub Desktop.
Save JamesHarrison/2891708 to your computer and use it in GitHub Desktop.
A simple script that optimises images in the current folder using optipng, gifsicle and jpegoptim, plus exiftool for metadata
#!/usr/bin/env ruby
# Recursively optimises in-place all images in a directory tree, starting in the current folder or a folder specified on the command line
# Dependencies:
# * optipng
# * gifscicle
# * jpegoptim
# * exiftool
# Recursively optimises a given path
def recursively_optimise(path,dt,depth=0)
raise ArgumentError, "Argument must be a directory" unless File.directory?(path)
puts "Called with #{path}"
total_before = 0
total_after = 0
total_before_types = {}
total_after_types = {}
total_time_types = {}
begin
# Call ourself on any directories in this path
Dir.foreach(path) do |entry|
next if entry == "."
next if entry == ".."
fullpath = File.join(path, entry)
if File.directory?(fullpath)
res = recursively_optimise(fullpath, dt, (depth+1))
total_before += res[:before]
total_after += res[:after]
#if res[:before_types] and res[:after_types]
res[:before_types].each_pair do |k,v|
total_before_types[k] = 0 unless total_before_types[k]
total_before_types[k] += v
end
res[:after_types].each_pair do |k,v|
total_after_types[k] = 0 unless total_after_types[k]
total_after_types[k] += v
end
res[:time_types].each_pair do |k,v|
total_time_types[k] = 0 unless total_time_types[k]
total_time_types[k] += v
end
#end
else
res = optimise_image(fullpath, dt)
total_before += res[:before]
total_after += res[:after]
total_before_types[res[:type]] = 0 unless total_before_types[res[:type]]
total_before_types[res[:type]] += res[:before]
total_after_types[res[:type]] = 0 unless total_after_types[res[:type]]
total_after_types[res[:type]] += res[:after]
total_time_types[res[:type]] = 0 unless total_time_types[res[:type]]
total_time_types[res[:type]] += res[:time]
end
end
rescue Exception => e
dt.write_to_disk
puts "Got exception #{e.inspect} while processing!"
puts e.backtrace
end
if depth == 0
puts "\n\nDone optimising everything!"
puts "I started with a total size of #{total_before.to_human}"
puts "I finished with a total size of #{total_after.to_human}, a reduction of #{(total_before-total_after).abs.to_human}"
puts"\nType breakdowns:"
total_before_types.each_pair do |k,v|
a = total_after_types[k]
t = total_time_types[k]
puts "#{k.to_s.upcase}: #{v.to_human} before, #{a.to_human} after, reduction of #{(v-a).abs.to_human} or #{(((v-a).abs)/t.abs).to_human} per second saved"
end
end
dt.write_to_disk
return {:before => total_before, :after => total_after, :before_types => total_before_types, :after_types => total_after_types, :time_types => total_time_types}
end
# Optimises a single image
def optimise_image(path,dt)
# Tweak commands here if you want to. Defaults are needlessly overzealous and will take ages, especially PNG.
png_cmd = "optipng -i0 -fix -o7 -preserve"
gif_cmd = "gifsicle -O3"
jpg_cmd = "jpegoptim --max=95 -p"
start = Time.now
start_size = File.size(path)
if dt.is_done?(path)
puts "Skipping image at #{path}, already done"
else
puts "Optimising image at #{path}, start filesize #{start_size.to_human}"
# let's figure out what we've got
ext = File.extname(path).downcase
type = :unknown
if ext == ".png"
`#{png_cmd} "#{path}"`
type = :png
dt.mark_done(path)
elsif ext == ".gif"
type = :gif
# ooh, okay, so if we're a gif, are we animated?
eto = `exiftool "#{path}"`
et = eto.split("\n")
fc = et.detect{|l|l.include?("Frame Count")}
if fc
if (fc.split(":")[1].to_i rescue 1) > 0
# We have more than one frame! We're animated or strange. gifsicle.
`#{gif_cmd} "#{path}"`
dt.mark_done(path)
else
# We're single frame, PNG probably does better
`#{png_cmd} "#{path}"`
pngpath = path.gsub(File.extname(path),".png")
if File.size(path) > File.size(pngpath)
# We're done! Nuke the old file
File.delete(path)
# Changed format, so update path
path = pngpath
dt.mark_done(path)
else
# Clean up the PNG we tried and gifsicle it.
File.delete(path.gsub(File.extname(path),".png"))
`#{gif_cmd} "#{path}"`
dt.mark_done(path)
end
end
else
# If we have no frame count data, assume not animated
`#{png_cmd} "#{path}"`
pngpath = path.gsub(File.extname(path),".png")
if File.size(path) > File.size(pngpath)
# We're done! Nuke the old file
File.delete(path)
# Changed format, so update path
path = pngpath
dt.mark_done(path)
else
# Clean up the PNG we tried and gifsicle it.
File.delete(path.gsub(File.extname(path),".png"))
`#{gif_cmd} "#{path}"`
dt.mark_done(path)
end
end
elsif ext == ".jpg" or ext == ".jpeg"
type = :jpg
`#{jpg_cmd} "#{path}"`
dt.mark_done(path)
else
puts "Skipped file, not a recognised file type"
end
end
return {:before=>start_size, :after=>File.size(path), :type=>type, :time=>(Time.now-start)}
end
class Numeric
def to_human
units = %w{B KB MB GB TB}
if self > 0
e = (Math.log(self)/Math.log(1024)).floor
s = "%.3f" % (to_f / 1024**e)
return s.sub(/\.?0*$/, units[e])
else
return "0 B"
end
end
end
class DoneTracker
def initialize
@path = File.expand_path("~/.optimdone.dat")
@done = []
File.open(@path,'r'){|f|@done = f.read.split("\n")} rescue nil
puts "Loaded #{@done.size} entries that we have already optimised"
#puts "#{@done.inspect}"
end
def mark_done(path)
cleanpath = path.gsub("\n","").gsub("\r","").chomp
unless @done.include?(cleanpath)
@done << path
end
end
def is_done?(path)
cleanpath = path.gsub("\n","").gsub("\r","").chomp
@done.include?(cleanpath)
end
def write_to_disk
#puts "Writing #{@done.inspect}"
File.open(@path,'w'){|f|f<<@done.join("\n")}
puts "Wrote #{@done.size} entries to disk"
end
end
dt = DoneTracker.new
start = Time.now
res = nil
if ARGV[0]
res = recursively_optimise(ARGV[0],dt)
else
res = recursively_optimise(Dir.getwd,dt)
end
p res
finish = Time.now
puts "Finished in #{finish-start} seconds, or approximately saving #{((res[:before]-res[:after]).abs/(finish-start).abs).to_human} per second"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment