image_squeeze 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/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
|
+
|