image_squeeze 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTORS +16 -0
- data/LICENSE +19 -0
- data/README.md +30 -0
- data/Rakefile +40 -0
- data/lib/image_squeeze/image_identifier.rb +24 -0
- data/lib/image_squeeze/log_factory.rb +14 -0
- data/lib/image_squeeze/processors/gif_to_png_processor.rb +19 -0
- data/lib/image_squeeze/processors/gifsicle_processor.rb +11 -0
- data/lib/image_squeeze/processors/jpeg_tran_non_progressive_processor.rb +11 -0
- data/lib/image_squeeze/processors/jpeg_tran_progressive_processor.rb +11 -0
- data/lib/image_squeeze/processors/png_crush_processor.rb +11 -0
- data/lib/image_squeeze/processors/processor.rb +37 -0
- data/lib/image_squeeze/result.rb +20 -0
- data/lib/image_squeeze/utils.rb +13 -0
- data/lib/image_squeeze.rb +108 -0
- data/test/fixtures/already_optimized_animated_gif.gif +0 -0
- data/test/fixtures/already_optimized_gif.gif +0 -0
- data/test/fixtures/already_optimized_jpg.jpg +0 -0
- data/test/fixtures/already_optimized_png.png +0 -0
- data/test/fixtures/better_as_progressive.jpg +0 -0
- data/test/fixtures/better_without_progressive.jpg +0 -0
- data/test/fixtures/gif.gif.gif.gif +0 -0
- data/test/fixtures/mislabeled_gif.png +0 -0
- data/test/fixtures/textfile.txt +0 -0
- data/test/fixtures/unoptimized_animated_gif.gif +0 -0
- data/test/fixtures/unoptimized_gif.gif +0 -0
- data/test/fixtures/unoptimized_png.png +0 -0
- data/test/functional/default_processors_test.rb +60 -0
- data/test/functional/image_identifier_test.rb +39 -0
- data/test/functional/result_test.rb +11 -0
- data/test/functional/squeeze_test.rb +116 -0
- data/test/test_helper.rb +12 -0
- metadata +93 -0
data/CONTRIBUTORS
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Images files from the artwork at:
|
2
|
+
-------------------------------------------------------
|
3
|
+
|
4
|
+
- http://www.kongregate.com/collabs/art/spartan619/chillin-patato
|
5
|
+
- http://www.kongregate.com/collabs/art/ethaN12349/face
|
6
|
+
- http://www.kongregate.com/collabs/art/ETHANR26/super-lightningrod-guitar-gif
|
7
|
+
- http://www.kongregate.com/collabs/art/DeathQueen/dq
|
8
|
+
- http://www.kongregate.com/collabs/art/nygiantsroxursox/the-beach
|
9
|
+
|
10
|
+
Additional thanks to:
|
11
|
+
-------------------------------------------------------
|
12
|
+
|
13
|
+
- Nicole Sullivan and Stoyan Stefanov for the idea (http://www.smushit.com/ysmush.it/)
|
14
|
+
- Steve Souders and all the authors of Even Faster Web Sites
|
15
|
+
- Duncan Beevers
|
16
|
+
- All the cool guys at Kongregate
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Andrew Grim, Kongregate
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# image_squeeze
|
2
|
+
|
3
|
+
A library for automated lossless image optimization
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
The default processors depend on ImageMagick, pngcrush, gifsicle, and jpegtran. ImageMagick is required for all processors.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
# set up an ImageSqueeze with the default processors
|
12
|
+
squeezer = ImageSqueeze.new(:default_processors => true)
|
13
|
+
|
14
|
+
# in-place squeeze of our png
|
15
|
+
squeezer.squeeze!('my_logo.png')
|
16
|
+
|
17
|
+
# non-destructive squeeze
|
18
|
+
result = squeezer.squeeze('your_logo.png')
|
19
|
+
puts "result saved #{result.bytes_saved} bytes, new image located at #{result.output_filename}"
|
20
|
+
|
21
|
+
# move tmp file to final location
|
22
|
+
squeezer.finalize_results(result)
|
23
|
+
|
24
|
+
## TODO
|
25
|
+
|
26
|
+
* Command line runner
|
27
|
+
|
28
|
+
## Copyright
|
29
|
+
|
30
|
+
Copyright (c) 2010 Andrew Grim. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'lib/image_squeeze'
|
5
|
+
|
6
|
+
desc "Run all tests"
|
7
|
+
task 'default' => ['test:functionals']
|
8
|
+
|
9
|
+
namespace 'test' do
|
10
|
+
functional_tests = FileList['test/functional/**/*_test.rb']
|
11
|
+
|
12
|
+
desc "Run functional tests"
|
13
|
+
Rake::TestTask.new('functionals') do |t|
|
14
|
+
t.libs << 'test'
|
15
|
+
t.test_files = functional_tests
|
16
|
+
t.verbose = true
|
17
|
+
t.warning = false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
spec = Gem::Specification.new do |s|
|
22
|
+
s.platform = Gem::Platform::RUBY
|
23
|
+
s.summary = "a library for automated lossless image optimization"
|
24
|
+
s.name = 'image_squeeze'
|
25
|
+
s.version = ImageSqueeze::VERSION
|
26
|
+
s.requirements << 'none'
|
27
|
+
s.require_path = 'lib'
|
28
|
+
s.files = FileList[
|
29
|
+
"lib/**/*", "test/**/*", "[a-zA-Z]*"
|
30
|
+
].exclude(/\.gitignore/)
|
31
|
+
s.description = "a library for automated lossless image optimization"
|
32
|
+
s.homepage = "http://github.com/stopdropandrew/image_squeeze"
|
33
|
+
s.author = "Andrew Grim"
|
34
|
+
s.email = "stopdropandrew@gmail.com"
|
35
|
+
end
|
36
|
+
|
37
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
38
|
+
pkg.need_zip = true
|
39
|
+
pkg.need_tar = true
|
40
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ImageSqueeze
|
2
|
+
def self.image_type(filename)
|
3
|
+
return ImageSqueeze::NOT_FOUND unless File.exist?(filename)
|
4
|
+
|
5
|
+
case identified_format(filename)
|
6
|
+
when /^GIFGIF/
|
7
|
+
ImageSqueeze::ANIMATED_GIF
|
8
|
+
when /GIF/
|
9
|
+
ImageSqueeze::GIF
|
10
|
+
when /JPEG/
|
11
|
+
ImageSqueeze::JPEG
|
12
|
+
when /PNG/
|
13
|
+
ImageSqueeze::PNG
|
14
|
+
else
|
15
|
+
ImageSqueeze::UNKNOWN
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def self.identified_format(filename)
|
22
|
+
`identify -format %m #{filename} 2> /dev/null`
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class ImageSqueeze
|
2
|
+
class LogFactory
|
3
|
+
def self.logger
|
4
|
+
@logger ||= create_logger
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
def self.create_logger
|
9
|
+
return Rails.logger if defined?(Rails) && defined?(Rails.logger)
|
10
|
+
return RAILS_DEFAULT_LOGGER if defined?(RAILS_DEFAULT_LOGGER)
|
11
|
+
return Logger.new(STDERR)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class ImageSqueeze
|
2
|
+
class GIFToPNGProcessor < Processor
|
3
|
+
def self.input_type
|
4
|
+
GIF
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.output_extension
|
8
|
+
'.png'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.squeeze(filename, output_filename)
|
12
|
+
intermediate_tmp_filename = tmp_filename(filename)
|
13
|
+
|
14
|
+
system("convert #{filename} PNG:#{intermediate_tmp_filename} 2> /dev/null")
|
15
|
+
|
16
|
+
PNGCrushProcessor.squeeze(intermediate_tmp_filename, output_filename) # run it through PNGCrush afterwards
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
class ImageSqueeze
|
4
|
+
class Processor
|
5
|
+
attr_reader :filename
|
6
|
+
|
7
|
+
def self.tmp_filename(filename)
|
8
|
+
t = Time.now.strftime("%Y%m%d")
|
9
|
+
path = "#{File.basename(filename)}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
|
10
|
+
|
11
|
+
File.join(Dir::tmpdir, path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.squeeze_to_tmp(filename)
|
15
|
+
tmp = tmp_filename(filename)
|
16
|
+
squeeze(filename, tmp)
|
17
|
+
tmp
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.squeeze(filename, output_filename)
|
21
|
+
raise "#{to_s}#squeeze should be defined in subclass and should convert filename to something at output_filename"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.handles?(image_type)
|
25
|
+
image_type == input_type
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.input_type
|
29
|
+
raise "#{to_s}#input_type should be defined in subclass"
|
30
|
+
end
|
31
|
+
|
32
|
+
# override if you want a different output extensions
|
33
|
+
def self.output_extension
|
34
|
+
ImageSqueeze::IMAGE_TYPE_TO_EXTENSION[input_type]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ImageSqueeze
|
2
|
+
class Result
|
3
|
+
attr_reader :filename, :output_filename, :bytes_saved, :output_extension
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@filename = options[:filename]
|
7
|
+
@output_filename = options[:output_filename]
|
8
|
+
@bytes_saved = options[:bytes_saved]
|
9
|
+
@output_extension = options[:output_extension]
|
10
|
+
end
|
11
|
+
|
12
|
+
def optimized?
|
13
|
+
bytes_saved.to_i > 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def <=>(other)
|
17
|
+
self.bytes_saved <=> other.bytes_saved
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ImageSqueeze
|
2
|
+
module Utils
|
3
|
+
def self.image_utility_available?(bin, extension, log_level = Logger::WARN)
|
4
|
+
return true if system("which #{bin} > /dev/null")
|
5
|
+
if log_level >= Logger::ERROR
|
6
|
+
ImageSqueeze.logger.error("#{bin} utility is required for running ImageSqueeze, get it installed already")
|
7
|
+
raise StandardError, "#{bin} utility is required for running ImageSqueeze, get it installed already"
|
8
|
+
else
|
9
|
+
ImageSqueeze.logger.log(log_level, "#{bin} utility could not be found, your #{extension} files won't be squeezed")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'image_squeeze/log_factory'
|
5
|
+
require 'image_squeeze/utils'
|
6
|
+
require 'image_squeeze/image_identifier'
|
7
|
+
require 'image_squeeze/result'
|
8
|
+
|
9
|
+
# processors
|
10
|
+
require 'image_squeeze/processors/processor'
|
11
|
+
require 'image_squeeze/processors/png_crush_processor'
|
12
|
+
require 'image_squeeze/processors/jpeg_tran_progressive_processor'
|
13
|
+
require 'image_squeeze/processors/jpeg_tran_non_progressive_processor'
|
14
|
+
require 'image_squeeze/processors/gifsicle_processor'
|
15
|
+
require 'image_squeeze/processors/gif_to_png_processor'
|
16
|
+
|
17
|
+
class ImageSqueeze
|
18
|
+
attr_reader :processors
|
19
|
+
|
20
|
+
VERSION = '0.1.0'
|
21
|
+
|
22
|
+
# Image Types
|
23
|
+
GIF = 'gif'
|
24
|
+
ANIMATED_GIF = 'animated gif'
|
25
|
+
JPEG = 'jpeg'
|
26
|
+
PNG = 'png'
|
27
|
+
UNKNOWN = 'unknown'
|
28
|
+
NOT_FOUND = 'not_found'
|
29
|
+
|
30
|
+
IMAGE_TYPE_TO_EXTENSION = {
|
31
|
+
GIF => '.gif',
|
32
|
+
ANIMATED_GIF => '.gif',
|
33
|
+
JPEG => '.jpg',
|
34
|
+
PNG => '.png'
|
35
|
+
}
|
36
|
+
|
37
|
+
def initialize(options = {})
|
38
|
+
# identify is required
|
39
|
+
ImageSqueeze::Utils.image_utility_available?('identify', 'all image', Logger::ERROR)
|
40
|
+
|
41
|
+
@processors = options[:processors] || []
|
42
|
+
@processors += self.class.default_processors if options[:default_processors]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.default
|
46
|
+
@processors = self.class.default_processors
|
47
|
+
end
|
48
|
+
|
49
|
+
def squeeze(filename)
|
50
|
+
image_type = self.class.image_type(filename)
|
51
|
+
return if [UNKNOWN, NOT_FOUND].include?(image_type)
|
52
|
+
|
53
|
+
processors = @processors.select{ |processor| processor.handles?(image_type) }
|
54
|
+
return if processors.empty?
|
55
|
+
|
56
|
+
original_file_size = File.size(filename)
|
57
|
+
sorted_results = processors.map do |processor_class|
|
58
|
+
output_filename = processor_class.squeeze_to_tmp(filename)
|
59
|
+
output_file_size = File.size(output_filename)
|
60
|
+
result_options = { :filename => filename, :output_filename => output_filename, :bytes_saved => original_file_size - output_file_size, :output_extension => processor_class.output_extension }
|
61
|
+
Result.new(result_options)
|
62
|
+
end.sort
|
63
|
+
|
64
|
+
most_optimized = sorted_results.pop if sorted_results[-1].optimized?
|
65
|
+
sorted_results.each do |result|
|
66
|
+
FileUtils.rm(result.output_filename)
|
67
|
+
end
|
68
|
+
most_optimized
|
69
|
+
end
|
70
|
+
|
71
|
+
def squeeze!(filename)
|
72
|
+
result = squeeze(filename)
|
73
|
+
|
74
|
+
output_filename = filename
|
75
|
+
output_filename = finalize_result(result) if result
|
76
|
+
end
|
77
|
+
|
78
|
+
def finalize_result(result)
|
79
|
+
filename = output_filename = result.filename
|
80
|
+
if File.extname(result.filename) != result.output_extension
|
81
|
+
output_filename = filename.sub(Regexp.new(Regexp.escape(File.extname(result.filename)) + '$'), result.output_extension)
|
82
|
+
FileUtils.cp(result.output_filename, output_filename)
|
83
|
+
FileUtils.rm(result.filename)
|
84
|
+
else
|
85
|
+
FileUtils.cp(result.output_filename, result.filename)
|
86
|
+
end
|
87
|
+
output_filename
|
88
|
+
end
|
89
|
+
|
90
|
+
def logger
|
91
|
+
LogFactory.logger
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.default_processors
|
95
|
+
processors = []
|
96
|
+
ImageSqueeze::Utils.image_utility_available?('identify', 'all image', Logger::ERROR)
|
97
|
+
if ImageSqueeze::Utils.image_utility_available?('pngcrush', 'pngs and gif', Logger::WARN)
|
98
|
+
processors << PNGCrushProcessor
|
99
|
+
processors << GIFToPNGProcessor if ImageSqueeze::Utils.image_utility_available?('convert', 'gif', Logger::WARN)
|
100
|
+
end
|
101
|
+
processors << GifsicleProcessor if ImageSqueeze::Utils.image_utility_available?('gifsicle', 'animated gif', Logger::WARN)
|
102
|
+
if ImageSqueeze::Utils.image_utility_available?('jpegtran', 'jpeg', Logger::WARN)
|
103
|
+
processors << JPEGTranProgressiveProcessor
|
104
|
+
processors << JPEGTranNonProgressiveProcessor
|
105
|
+
end
|
106
|
+
processors
|
107
|
+
end
|
108
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
File without changes
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'test_helper')
|
2
|
+
|
3
|
+
class DefaultProcessorsTest < Test::Unit::TestCase
|
4
|
+
def test_png_crush_processor_with_unoptimize_png
|
5
|
+
assert_processor_optimizes_file(ImageSqueeze::PNGCrushProcessor, 'unoptimized_png.png')
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_png_crush_processor_with_optimized_png
|
9
|
+
assert_processor_doesnt_optimize_file(ImageSqueeze::PNGCrushProcessor, 'already_optimized_png.png')
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_jpegtran_progressive_processor_with_already_optimized
|
13
|
+
assert_processor_doesnt_optimize_file(ImageSqueeze::JPEGTranProgressiveProcessor, 'already_optimized_jpg.jpg')
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_jpegtran_progressive_processor_with_progressive_optimized
|
17
|
+
assert_processor_optimizes_file(ImageSqueeze::JPEGTranProgressiveProcessor, 'better_as_progressive.jpg')
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_jpegtran_non_progressive_processor_with_already_optimized
|
21
|
+
assert_processor_doesnt_optimize_file(ImageSqueeze::JPEGTranNonProgressiveProcessor, 'already_optimized_jpg.jpg')
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_jpegtran_non_progressive_processor_with_progressive_optimized
|
25
|
+
assert_processor_optimizes_file(ImageSqueeze::JPEGTranNonProgressiveProcessor, 'better_without_progressive.jpg')
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_gifsicle_processor_with_unoptimized_animated_gif
|
29
|
+
assert_processor_optimizes_file(ImageSqueeze::GifsicleProcessor, 'unoptimized_animated_gif.gif')
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_gifsicle_processor_with_already_optimized_animated_gif
|
33
|
+
assert_processor_doesnt_optimize_file(ImageSqueeze::GifsicleProcessor, 'already_optimized_animated_gif.gif')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_gif_to_png_processor_with_unoptimized_gif
|
37
|
+
assert_processor_optimizes_file(ImageSqueeze::GIFToPNGProcessor, 'unoptimized_gif.gif')
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_gif_to_png_processor_with_already_optimized_animated_gif
|
41
|
+
assert_processor_doesnt_optimize_file(ImageSqueeze::GIFToPNGProcessor, 'already_optimized_gif.gif')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def assert_processor_optimizes_file(processor, file)
|
46
|
+
squeezer = ImageSqueeze.new(:processors => [processor])
|
47
|
+
filename = fixtures(file)
|
48
|
+
old_size = File.size(filename)
|
49
|
+
new_filename = squeezer.squeeze!(filename)
|
50
|
+
assert File.size(new_filename) < old_size, "New file size of #{File.size(new_filename)} should be smaller than original of #{old_size}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def assert_processor_doesnt_optimize_file(processor, file)
|
54
|
+
squeezer = ImageSqueeze.new(:processors => [processor])
|
55
|
+
filename = fixtures(file)
|
56
|
+
old_size = File.size(filename)
|
57
|
+
new_filename = squeezer.squeeze!(filename)
|
58
|
+
assert File.size(new_filename) >= old_size, "New file size of #{File.size(new_filename)} should be at least as big as original of #{old_size}"
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'test_helper')
|
2
|
+
|
3
|
+
class ImageTypeTest < Test::Unit::TestCase
|
4
|
+
def test_image_type_not_found_when_no_file
|
5
|
+
image_type = ImageSqueeze.image_type('blah.jpg')
|
6
|
+
assert_equal ImageSqueeze::NOT_FOUND, image_type
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_image_type_correctly_identifies_gif
|
10
|
+
image_type = ImageSqueeze.image_type(fixtures('already_optimized_gif.gif'))
|
11
|
+
assert_equal ImageSqueeze::GIF, image_type
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_image_type_correctly_identifies_animated_gif
|
15
|
+
image_type = ImageSqueeze.image_type(fixtures('unoptimized_animated_gif.gif'))
|
16
|
+
assert_equal ImageSqueeze::ANIMATED_GIF, image_type
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_image_type_correctly_identifies_png
|
20
|
+
image_type = ImageSqueeze.image_type(fixtures('already_optimized_png.png'))
|
21
|
+
assert_equal ImageSqueeze::PNG, image_type
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_image_type_correctly_identifies_jpeg
|
25
|
+
image_type = ImageSqueeze.image_type(fixtures('already_optimized_jpg.jpg'))
|
26
|
+
assert_equal ImageSqueeze::JPEG, image_type
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_image_type_correctly_identifies_non_images
|
30
|
+
image_type = ImageSqueeze.image_type(fixtures('textfile.txt'))
|
31
|
+
assert_equal ImageSqueeze::UNKNOWN, image_type
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_image_type_correctly_identifies_image_with_wrong_extension
|
35
|
+
image_type = ImageSqueeze.image_type(fixtures('mislabeled_gif.png'))
|
36
|
+
assert_equal ImageSqueeze::GIF, image_type
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'test_helper')
|
2
|
+
|
3
|
+
class ResultTest < Test::Unit::TestCase
|
4
|
+
def test_results_sort_by_bytes_saved
|
5
|
+
small = ImageSqueeze::Result.new(:bytes_saved => -10)
|
6
|
+
medium = ImageSqueeze::Result.new(:bytes_saved => 20)
|
7
|
+
big = ImageSqueeze::Result.new(:bytes_saved => 100)
|
8
|
+
|
9
|
+
assert_equal [small, medium, big], [medium, big, small].sort
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'test_helper')
|
2
|
+
require 'mocha'
|
3
|
+
|
4
|
+
class SqueezeTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@image_squeezer = ImageSqueeze.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_squeeze_results_in_unoptimized_for_unknown_file_type
|
10
|
+
assert !@image_squeezer.squeeze(__FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_squeeze_result_is_unoptimized_for_file_not_found
|
14
|
+
assert !@image_squeezer.squeeze('thisisnotarealfile.jjj')
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_squeeze_a_image_without_a_processor
|
18
|
+
assert !@image_squeezer.squeeze(fixtures('already_optimized_gif.gif'))
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_squeeze_result_is_unoptimized_when_no_optimization_is_made
|
22
|
+
image_squeezer = custom_image_squeezer(NeverOptimize)
|
23
|
+
assert !image_squeezer.squeeze(fixtures('already_optimized_gif.gif'))
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_squeeze_result_is_deleted_when_no_optimization_is_made
|
27
|
+
image_squeezer = custom_image_squeezer(NeverOptimize)
|
28
|
+
NeverOptimize.stubs(:tmp_filename).returns(output_filename = File.join(File.dirname(__FILE__), '..', 'tmp', 'tmpfile'))
|
29
|
+
assert !image_squeezer.squeeze(fixtures('already_optimized_gif.gif'))
|
30
|
+
assert !File.exists?(output_filename)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_squeeze_result_is_optimized_when_an_optimization_is_made
|
34
|
+
image_squeezer = custom_image_squeezer(AlwaysOptimize)
|
35
|
+
result = image_squeezer.squeeze(fixtures('already_optimized_gif.gif'))
|
36
|
+
assert result.optimized?
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_squeeze_picks_the_best_result_when_processors_return_different_sizes
|
40
|
+
image_squeezer = custom_image_squeezer(AlwaysOptimize, NeverOptimize)
|
41
|
+
result = image_squeezer.squeeze(fixtures('already_optimized_gif.gif'))
|
42
|
+
assert result.optimized?
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_squeezebang_overwrites_original_file
|
46
|
+
image_squeezer = custom_image_squeezer(AlwaysOptimize)
|
47
|
+
filename = fixtures('already_optimized_gif.gif')
|
48
|
+
old_size = File.size(filename)
|
49
|
+
result = image_squeezer.squeeze!(filename)
|
50
|
+
|
51
|
+
assert File.size(filename) < old_size
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_squeezebang_removes_old_file_when_extension_is_changed
|
55
|
+
image_squeezer = custom_image_squeezer(PNGOutput)
|
56
|
+
filename = fixtures('already_optimized_gif.gif')
|
57
|
+
result = image_squeezer.squeeze!(filename)
|
58
|
+
|
59
|
+
assert !File.exists?(filename), "Old file should be gone"
|
60
|
+
assert File.exists?(filename.gsub(/\.gif/, '.png')), "New file should exist"
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_squeezebang_handles_changing_extension_when_dot_extension_is_repeated
|
64
|
+
image_squeezer = custom_image_squeezer(PNGOutput)
|
65
|
+
filename = fixtures('gif.gif.gif.gif')
|
66
|
+
result = image_squeezer.squeeze!(filename)
|
67
|
+
|
68
|
+
assert !File.exists?(filename), "Old file should be gone"
|
69
|
+
assert File.exists?(filename.gsub(/\.gif$/, '.png')), "New file should exist"
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_squeezebang_doesnt_overwrite_when_file_not_optimized
|
73
|
+
image_squeezer = custom_image_squeezer(AlwaysBigger)
|
74
|
+
filename = fixtures('already_optimized_gif.gif')
|
75
|
+
old_size = File.size(filename)
|
76
|
+
result = image_squeezer.squeeze!(filename)
|
77
|
+
assert_equal old_size, File.size(filename)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
class AlwaysOptimize < ImageSqueeze::Processor
|
82
|
+
def self.squeeze(filename, output_filename)
|
83
|
+
`echo 'real small' > #{output_filename}` # this will make the new file really small
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.input_type; ImageSqueeze::GIF; end
|
87
|
+
end
|
88
|
+
|
89
|
+
class NeverOptimize < ImageSqueeze::Processor
|
90
|
+
def self.squeeze(filename, output_filename)
|
91
|
+
`cp #{filename} #{output_filename}`
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.input_type; ImageSqueeze::GIF; end
|
95
|
+
end
|
96
|
+
|
97
|
+
class AlwaysBigger < ImageSqueeze::Processor
|
98
|
+
def self.squeeze(filename, output_filename)
|
99
|
+
`cp #{filename} #{output_filename}; echo "hi" >> #{output_filename}`
|
100
|
+
end
|
101
|
+
def self.input_type; ImageSqueeze::GIF; end
|
102
|
+
end
|
103
|
+
|
104
|
+
class PNGOutput < ImageSqueeze::Processor
|
105
|
+
def self.squeeze(filename, output_filename)
|
106
|
+
`echo 'real small' > #{output_filename}` # this will make the new file really small
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.input_type; ImageSqueeze::GIF; end
|
110
|
+
def self.output_extension; '.png'; end
|
111
|
+
end
|
112
|
+
|
113
|
+
def custom_image_squeezer(*processors)
|
114
|
+
ImageSqueeze.new(:processors => processors )
|
115
|
+
end
|
116
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'ruby-debug'
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'image_squeeze')
|
4
|
+
|
5
|
+
class Test::Unit::TestCase
|
6
|
+
def fixtures(filename)
|
7
|
+
original_file = File.join(File.dirname(__FILE__), 'fixtures', filename)
|
8
|
+
new_file = File.join(Dir::tmpdir, filename)
|
9
|
+
FileUtils.cp(original_file, new_file)
|
10
|
+
new_file
|
11
|
+
end
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: image_squeeze
|
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
|
+
- Andrew Grim
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-17 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: a library for automated lossless image optimization
|
22
|
+
email: stopdropandrew@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/image_squeeze/image_identifier.rb
|
31
|
+
- lib/image_squeeze/log_factory.rb
|
32
|
+
- lib/image_squeeze/processors/gif_to_png_processor.rb
|
33
|
+
- lib/image_squeeze/processors/gifsicle_processor.rb
|
34
|
+
- lib/image_squeeze/processors/jpeg_tran_non_progressive_processor.rb
|
35
|
+
- lib/image_squeeze/processors/jpeg_tran_progressive_processor.rb
|
36
|
+
- lib/image_squeeze/processors/png_crush_processor.rb
|
37
|
+
- lib/image_squeeze/processors/processor.rb
|
38
|
+
- lib/image_squeeze/result.rb
|
39
|
+
- lib/image_squeeze/utils.rb
|
40
|
+
- lib/image_squeeze.rb
|
41
|
+
- test/fixtures/already_optimized_animated_gif.gif
|
42
|
+
- test/fixtures/already_optimized_gif.gif
|
43
|
+
- test/fixtures/already_optimized_jpg.jpg
|
44
|
+
- test/fixtures/already_optimized_png.png
|
45
|
+
- test/fixtures/better_as_progressive.jpg
|
46
|
+
- test/fixtures/better_without_progressive.jpg
|
47
|
+
- test/fixtures/gif.gif.gif.gif
|
48
|
+
- test/fixtures/mislabeled_gif.png
|
49
|
+
- test/fixtures/textfile.txt
|
50
|
+
- test/fixtures/unoptimized_animated_gif.gif
|
51
|
+
- test/fixtures/unoptimized_gif.gif
|
52
|
+
- test/fixtures/unoptimized_png.png
|
53
|
+
- test/functional/default_processors_test.rb
|
54
|
+
- test/functional/image_identifier_test.rb
|
55
|
+
- test/functional/result_test.rb
|
56
|
+
- test/functional/squeeze_test.rb
|
57
|
+
- test/test_helper.rb
|
58
|
+
- CONTRIBUTORS
|
59
|
+
- LICENSE
|
60
|
+
- Rakefile
|
61
|
+
- README.md
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/stopdropandrew/image_squeeze
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
requirements:
|
86
|
+
- none
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.3.6
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: a library for automated lossless image optimization
|
92
|
+
test_files: []
|
93
|
+
|