rarity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/Rakefile +2 -0
- data/bin/rarity +52 -0
- data/lib/rarity/optimiser.rb +80 -0
- data/lib/rarity/runner.rb +74 -0
- data/lib/rarity/tracker.rb +46 -0
- data/lib/rarity/version.rb +3 -0
- data/lib/rarity.rb +17 -0
- data/rarity.gemspec +20 -0
- metadata +120 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 James Harrison
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Rarity - a recursive image optimiser
|
2
|
+
|
3
|
+
If you understand why you need this there's something wrong with you or you have very strange needs.
|
4
|
+
|
5
|
+
This is a tool built for a friend to recursively optimise a directory of images, keeping track of progress to support partial runs and update runs, using optipng, jpegoptim and gifsicle.
|
6
|
+
|
7
|
+
## Dependencies
|
8
|
+
|
9
|
+
Rarity makes use of:
|
10
|
+
|
11
|
+
* optipng
|
12
|
+
* jpegoptim
|
13
|
+
* gifsicle
|
14
|
+
|
15
|
+
You will need to install these prior to running rarity.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'rarity'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install rarity
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
Basic usage:
|
34
|
+
|
35
|
+
rarity optim -d some/path
|
36
|
+
|
37
|
+
Spike will then optimise everything under that directory.
|
38
|
+
|
39
|
+
You can find more help with:
|
40
|
+
|
41
|
+
rarity -h
|
42
|
+
rarity optim -h
|
43
|
+
rarity tracker -h
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
1. Fork it
|
48
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
49
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
50
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
51
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/rarity
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
5
|
+
require 'rarity'
|
6
|
+
|
7
|
+
SUB_COMMANDS = %w(optim tracker)
|
8
|
+
global_opts = Trollop::options do
|
9
|
+
version "Rarity #{Rarity::VERSION} (c) James Harrison"
|
10
|
+
banner <<-EOS
|
11
|
+
Rarity recursively walks a directory and optimises all the images contained therein.
|
12
|
+
|
13
|
+
Usage:
|
14
|
+
rarity optim -h
|
15
|
+
rarity tracker -h
|
16
|
+
|
17
|
+
Global options:
|
18
|
+
EOS
|
19
|
+
stop_on SUB_COMMANDS
|
20
|
+
end
|
21
|
+
|
22
|
+
cmd = ARGV.shift # get the subcommand
|
23
|
+
cmd_opts = case cmd
|
24
|
+
when "optim"
|
25
|
+
Trollop::options do
|
26
|
+
opt :pnglevel, "The argument to -o to use for pngcrush", :default => 3
|
27
|
+
opt :directory, "The top level of the directory tree to optimise", :type => String
|
28
|
+
end
|
29
|
+
when "tracker" # parse copy options
|
30
|
+
Trollop::options do
|
31
|
+
opt :reset, "Reset the tracker database"
|
32
|
+
opt :import, "Run an import from an old flat-file DB"
|
33
|
+
end
|
34
|
+
else
|
35
|
+
Trollop::die "unknown subcommand #{cmd.inspect}"
|
36
|
+
end
|
37
|
+
#puts "Subcommand options: #{cmd_opts.inspect}"
|
38
|
+
|
39
|
+
if cmd == "optim"
|
40
|
+
r = Rarity::Runner.new(cmd_opts)
|
41
|
+
r.run
|
42
|
+
elsif cmd == "tracker"
|
43
|
+
if cmd_opts[:reset]
|
44
|
+
t = Rarity::Tracker.new
|
45
|
+
t.reset
|
46
|
+
elsif cmd_opts[:import]
|
47
|
+
t = Rarity::Tracker.new
|
48
|
+
t.import_from_old_format
|
49
|
+
else
|
50
|
+
puts "I'm afraid you'll have to provide an option."
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class Rarity::Optimiser
|
2
|
+
def initialize(tracker, options)
|
3
|
+
@tracker = tracker
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
# Optimises a single image
|
7
|
+
def optimise_image(path)
|
8
|
+
# Tweak commands here if you want to. Defaults are needlessly overzealous and will take ages, especially PNG.
|
9
|
+
png_cmd = "optipng -i0 -fix -o#{@options[:png_o_level]} -preserve"
|
10
|
+
gif_cmd = "gifsicle -O3"
|
11
|
+
jpg_cmd = "jpegoptim --max=95 -p"
|
12
|
+
start = Time.now
|
13
|
+
start_size = File.size(path)
|
14
|
+
if @tracker.is_done?(path, @options)
|
15
|
+
puts "Skipping image at #{path}, already done"
|
16
|
+
else
|
17
|
+
puts "Optimising image at #{path}, start filesize #{Rarity::to_human start_size}"
|
18
|
+
# let's figure out what we've got
|
19
|
+
ext = File.extname(path).downcase
|
20
|
+
type = :unknown
|
21
|
+
if ext == ".png"
|
22
|
+
`#{png_cmd} "#{path}"`
|
23
|
+
type = :png
|
24
|
+
@tracker.mark_done(path, @options)
|
25
|
+
elsif ext == ".gif"
|
26
|
+
type = :gif
|
27
|
+
# ooh, okay, so if we're a gif, are we animated?
|
28
|
+
eto = `exiftool "#{path}"`
|
29
|
+
et = eto.split("\n")
|
30
|
+
fc = et.detect{|l|l.include?("Frame Count")}
|
31
|
+
if fc
|
32
|
+
if (fc.split(":")[1].to_i rescue 1) > 0
|
33
|
+
# We have more than one frame! We're animated or strange. gifsicle.
|
34
|
+
`#{gif_cmd} "#{path}"`
|
35
|
+
@tracker.mark_done(path, @options)
|
36
|
+
else
|
37
|
+
# We're single frame, PNG probably does better
|
38
|
+
opo = `#{png_cmd} "#{path}"`
|
39
|
+
pngpath = path.gsub(File.extname(path),".png")
|
40
|
+
if File.size(path) > File.size(pngpath)
|
41
|
+
# We're done! Nuke the old file
|
42
|
+
File.delete(path)
|
43
|
+
# Changed format, so update path
|
44
|
+
path = pngpath
|
45
|
+
@tracker.mark_done(path, @options)
|
46
|
+
else
|
47
|
+
# Clean up the PNG we tried and gifsicle it.
|
48
|
+
File.delete(path.gsub(File.extname(path),".png"))
|
49
|
+
`#{gif_cmd} "#{path}"`
|
50
|
+
@tracker.mark_done(path, @options)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
# If we have no frame count data, assume not animated
|
55
|
+
opo = `#{png_cmd} "#{path}"`
|
56
|
+
pngpath = path.gsub(File.extname(path),".png")
|
57
|
+
if File.size(path) > File.size(pngpath)
|
58
|
+
# We're done! Nuke the old file
|
59
|
+
File.delete(path)
|
60
|
+
# Changed format, so update path
|
61
|
+
path = pngpath
|
62
|
+
@tracker.mark_done(path, @options)
|
63
|
+
else
|
64
|
+
# Clean up the PNG we tried and gifsicle it.
|
65
|
+
File.delete(path.gsub(File.extname(path),".png"))
|
66
|
+
`#{gif_cmd} "#{path}"`
|
67
|
+
@tracker.mark_done(path, @options)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
elsif ext == ".jpg" or ext == ".jpeg"
|
71
|
+
type = :jpg
|
72
|
+
`#{jpg_cmd} "#{path}"`
|
73
|
+
@tracker.mark_done(path, @options)
|
74
|
+
else
|
75
|
+
puts "Skipped file, not a recognised file type"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return {:before=>start_size, :after=>File.size(path), :type=>type, :time=>(Time.now-start)}
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class Rarity::Runner
|
2
|
+
def initialize(options)
|
3
|
+
@directory = options[:directory]
|
4
|
+
@tracker = Rarity::Tracker.new
|
5
|
+
@optimiser = Rarity::Optimiser.new(@tracker, {:png_o_level => options[:pnglevel]})
|
6
|
+
end
|
7
|
+
def run
|
8
|
+
recursively_optimise(@directory)
|
9
|
+
end
|
10
|
+
# Recursively optimises a given path
|
11
|
+
def recursively_optimise(path,depth=0)
|
12
|
+
raise ArgumentError, "Argument must be a directory" unless File.directory?(path)
|
13
|
+
puts "Called with #{path}"
|
14
|
+
total_before = 0
|
15
|
+
total_after = 0
|
16
|
+
total_before_types = {}
|
17
|
+
total_after_types = {}
|
18
|
+
total_time_types = {}
|
19
|
+
begin
|
20
|
+
# Call ourself on any directories in this path
|
21
|
+
Dir.foreach(path) do |entry|
|
22
|
+
next if entry == "."
|
23
|
+
next if entry == ".."
|
24
|
+
fullpath = File.join(path, entry)
|
25
|
+
if File.directory?(fullpath)
|
26
|
+
res = self.recursively_optimise(fullpath, (depth+1))
|
27
|
+
total_before += res[:before]
|
28
|
+
total_after += res[:after]
|
29
|
+
#if res[:before_types] and res[:after_types]
|
30
|
+
res[:before_types].each_pair do |k,v|
|
31
|
+
total_before_types[k] = 0 unless total_before_types[k]
|
32
|
+
total_before_types[k] += v
|
33
|
+
end
|
34
|
+
res[:after_types].each_pair do |k,v|
|
35
|
+
total_after_types[k] = 0 unless total_after_types[k]
|
36
|
+
total_after_types[k] += v
|
37
|
+
end
|
38
|
+
res[:time_types].each_pair do |k,v|
|
39
|
+
total_time_types[k] = 0 unless total_time_types[k]
|
40
|
+
total_time_types[k] += v
|
41
|
+
end
|
42
|
+
#end
|
43
|
+
else
|
44
|
+
res = @optimiser.optimise_image(fullpath)
|
45
|
+
total_before += res[:before]
|
46
|
+
total_after += res[:after]
|
47
|
+
total_before_types[res[:type]] = 0 unless total_before_types[res[:type]]
|
48
|
+
total_before_types[res[:type]] += res[:before]
|
49
|
+
total_after_types[res[:type]] = 0 unless total_after_types[res[:type]]
|
50
|
+
total_after_types[res[:type]] += res[:after]
|
51
|
+
total_time_types[res[:type]] = 0 unless total_time_types[res[:type]]
|
52
|
+
total_time_types[res[:type]] += res[:time]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue Exception => e
|
56
|
+
puts "Got exception #{e.inspect} while processing!"
|
57
|
+
puts e.backtrace
|
58
|
+
end
|
59
|
+
if depth == 0
|
60
|
+
puts "\n\nDone optimising everything!"
|
61
|
+
puts "I started with a total size of #{Rarity::to_human total_before}"
|
62
|
+
puts "I finished with a total size of #{Rarity::to_human total_after}, a reduction of #{Rarity::to_human (total_before-total_after).abs}"
|
63
|
+
|
64
|
+
puts"\nType breakdowns:"
|
65
|
+
total_before_types.each_pair do |k,v|
|
66
|
+
a = total_after_types[k]
|
67
|
+
t = total_time_types[k]
|
68
|
+
puts "#{k.to_s.upcase}: #{Rarity::to_human v} before, #{Rarity::to_human a} after, reduction of #{Rarity::to_human (v-a).abs} or #{Rarity::to_human (((v-a).abs)/t.abs)} per second saved"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
return {:before => total_before, :after => total_after, :before_types => total_before_types, :after_types => total_after_types, :time_types => total_time_types}
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
class Rarity::Tracker
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@db_path = File.expand_path("~/.spike.sqlite")
|
6
|
+
@first_run = !File.exists?(@db_path)
|
7
|
+
@db = Sequel.sqlite(@db_path)
|
8
|
+
if @first_run
|
9
|
+
@db.create_table :images do
|
10
|
+
primary_key :id
|
11
|
+
String :path
|
12
|
+
Integer :png_o_level
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
def import_from_old_format
|
17
|
+
@path = File.expand_path("~/.optimdone.dat")
|
18
|
+
@done = []
|
19
|
+
File.open(@path,'r'){|f|@done = f.read.split("\n")} rescue nil
|
20
|
+
for r in @done
|
21
|
+
@db[:images].insert(:path=>r, :png_o_level=>3)
|
22
|
+
end
|
23
|
+
puts "Imported #{@done.size} entries that we have already optimised from the old land, you can now delete ~/.optimdone.dat"
|
24
|
+
end
|
25
|
+
def mark_done(path, options)
|
26
|
+
cleanpath = path.gsub("\n","").gsub("\r","").chomp
|
27
|
+
if !is_done?(path, options)
|
28
|
+
if @db[:images][:path=>cleanpath]
|
29
|
+
@db[:images][:path=>cleanpath] = {:png_o_level => options[:png_o_level]}
|
30
|
+
else
|
31
|
+
@db[:images].insert(:path=>cleanpath, :png_o_level=>options[:png_o_level])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
def is_done?(path, options)
|
36
|
+
cleanpath = path.gsub("\n","").gsub("\r","").chomp
|
37
|
+
if @db[:images].select(:id, :png_o_level).where(:path=>cleanpath).filter('png_o_level >= ?',options[:png_o_level]).count > 0
|
38
|
+
return true
|
39
|
+
end
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
def reset
|
43
|
+
@db[:images].delete
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/rarity.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rarity/version"
|
2
|
+
|
3
|
+
module Rarity
|
4
|
+
def self.to_human(number)
|
5
|
+
units = %w{B KB MB GB TB}
|
6
|
+
if number > 0
|
7
|
+
e = (Math.log(number)/Math.log(1024)).floor
|
8
|
+
s = "%.3f" % (number.to_f / 1024**e)
|
9
|
+
return s.sub(/\.?0*$/, units[e])
|
10
|
+
else
|
11
|
+
return "0 B"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
require "rarity/tracker"
|
16
|
+
require "rarity/optimiser"
|
17
|
+
require "rarity/runner"
|
data/rarity.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/rarity/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["James Harrison"]
|
6
|
+
gem.email = ["james@talkunafraid.co.uk"]
|
7
|
+
gem.description = %q{A tool for recursively optimising directories of images}
|
8
|
+
gem.summary = %q{This tool uses optipng, jpegoptim and gifsicle to optimise image file sizes for a directory tree.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "rarity"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Rarity::VERSION
|
17
|
+
gem.add_dependency("sqlite3", ">= 1.3.6")
|
18
|
+
gem.add_dependency("sequel", ">= 3.36.0")
|
19
|
+
gem.add_dependency("trollop", ">= 1.16.2")
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rarity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- James Harrison
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-06-14 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: sqlite3
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 3
|
31
|
+
- 6
|
32
|
+
version: 1.3.6
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: sequel
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 3
|
45
|
+
- 36
|
46
|
+
- 0
|
47
|
+
version: 3.36.0
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: trollop
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 1
|
60
|
+
- 16
|
61
|
+
- 2
|
62
|
+
version: 1.16.2
|
63
|
+
type: :runtime
|
64
|
+
version_requirements: *id003
|
65
|
+
description: A tool for recursively optimising directories of images
|
66
|
+
email:
|
67
|
+
- james@talkunafraid.co.uk
|
68
|
+
executables:
|
69
|
+
- rarity
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files: []
|
73
|
+
|
74
|
+
files:
|
75
|
+
- .gitignore
|
76
|
+
- Gemfile
|
77
|
+
- LICENSE
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- bin/rarity
|
81
|
+
- lib/rarity.rb
|
82
|
+
- lib/rarity/optimiser.rb
|
83
|
+
- lib/rarity/runner.rb
|
84
|
+
- lib/rarity/tracker.rb
|
85
|
+
- lib/rarity/version.rb
|
86
|
+
- rarity.gemspec
|
87
|
+
has_rdoc: true
|
88
|
+
homepage: ""
|
89
|
+
licenses: []
|
90
|
+
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.3.7
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: This tool uses optipng, jpegoptim and gifsicle to optimise image file sizes for a directory tree.
|
119
|
+
test_files: []
|
120
|
+
|