middleman-imageoptim 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +46 -0
- data/.simplecov +7 -0
- data/.travis.yml +1 -2
- data/Gemfile +1 -11
- data/README.md +35 -19
- data/Rakefile +11 -19
- data/features/manifest.feature +31 -0
- data/features/optimization.feature +10 -0
- data/features/permissions.feature +14 -0
- data/features/support/env.rb +7 -0
- data/features/support/step_definitions.rb +39 -0
- data/fixtures/basic-app/config-disabled-manifest.rb +3 -0
- data/fixtures/basic-app/config.rb +1 -0
- data/fixtures/basic-app/source/images/oh_my_glob.gif +0 -0
- data/fixtures/basic-app/source/images/table.jpg +0 -0
- data/lib/middleman-imageoptim.rb +12 -8
- data/lib/middleman-imageoptim/extension.rb +14 -13
- data/lib/middleman-imageoptim/manifest.rb +52 -0
- data/lib/middleman-imageoptim/manifest_resource.rb +41 -0
- data/lib/middleman-imageoptim/optimizer.rb +71 -74
- data/lib/middleman-imageoptim/options.rb +46 -56
- data/lib/middleman-imageoptim/resource_list.rb +72 -0
- data/lib/middleman-imageoptim/utils.rb +42 -0
- data/lib/middleman-imageoptim/version.rb +1 -1
- data/lib/middleman_extension.rb +1 -1
- data/middleman-imageoptim.gemspec +19 -16
- data/script/spec +20 -5
- data/spec/spec_helper.rb +4 -7
- data/spec/unit/options_spec.rb +97 -48
- data/spec/unit/utils_spec.rb +55 -0
- metadata +70 -16
- data/.cane +0 -4
- data/spec/unit/optimizer_spec.rb +0 -58
- data/spec/use_coveralls.rb +0 -2
- data/spec/use_simplecov.rb +0 -15
@@ -0,0 +1,41 @@
|
|
1
|
+
module Middleman
|
2
|
+
module Imageoptim
|
3
|
+
class ManifestResource < ::Middleman::Sitemap::Resource
|
4
|
+
attr_accessor :output
|
5
|
+
|
6
|
+
def template?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(*_args, &_block)
|
11
|
+
manifest_content
|
12
|
+
end
|
13
|
+
|
14
|
+
def binary?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def raw_data
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
|
22
|
+
def ignored?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def metadata
|
27
|
+
@local_metadata.dup
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def manifest_content
|
33
|
+
if @source_file.nil?
|
34
|
+
YAML.dump({})
|
35
|
+
else
|
36
|
+
File.read(@source_file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,107 +1,104 @@
|
|
1
1
|
module Middleman
|
2
2
|
module Imageoptim
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'image_optim'
|
4
|
+
require 'fileutils'
|
5
5
|
|
6
6
|
# Optimizer class that accepts an options object and processes files and
|
7
7
|
# passes them off to image_optim to be processed
|
8
8
|
class Optimizer
|
9
|
-
|
10
|
-
@app = app
|
11
|
-
@builder = builder
|
12
|
-
@options = options
|
13
|
-
@total_savings = 0
|
14
|
-
end
|
9
|
+
attr_reader :app, :builder, :options, :byte_savings
|
15
10
|
|
16
|
-
def optimize!
|
17
|
-
|
18
|
-
optimizer.optimize_images(images_to_optimize) {|src_file, dst_file|
|
19
|
-
if dst_file
|
20
|
-
@total_savings += (src_file.size - dst_file.size)
|
21
|
-
say_file_size_stats(src_file, dst_file)
|
22
|
-
FileUtils.mv dst_file, src_file
|
23
|
-
elsif @options.verbose
|
24
|
-
say_status "[skipped] #{src_file}"
|
25
|
-
end
|
26
|
-
}
|
27
|
-
say_status "Total image savings: #{format_size(@total_savings)}"
|
11
|
+
def self.optimize!(app, builder, options)
|
12
|
+
new(app, builder, options).process_images
|
28
13
|
end
|
29
14
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
15
|
+
def initialize(app, builder, options)
|
16
|
+
@app = app
|
17
|
+
@builder = builder
|
18
|
+
@options = options
|
19
|
+
@byte_savings = 0
|
34
20
|
end
|
35
21
|
|
36
|
-
def
|
37
|
-
|
22
|
+
def process_images
|
23
|
+
images = updated_images
|
24
|
+
modes = preoptimize_modes(images)
|
25
|
+
optimizer.optimize_images(images) do |source, destination|
|
26
|
+
process_image(source, destination, modes.fetch(source.to_s))
|
27
|
+
end
|
28
|
+
update_manifest
|
29
|
+
say_status 'Total savings: %{data}', data: Utils.format_size(byte_savings)
|
38
30
|
end
|
39
31
|
|
40
|
-
|
41
|
-
|
32
|
+
private
|
33
|
+
|
34
|
+
def update_manifest
|
35
|
+
return unless options.manifest
|
36
|
+
manifest.build_and_write(optimizable_images)
|
37
|
+
say_status '%{manifest_path} updated', manifest_path: manifest.path
|
42
38
|
end
|
43
39
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
'
|
48
|
-
|
49
|
-
'larger'
|
40
|
+
def process_image(source, destination = nil, mode = nil)
|
41
|
+
if destination
|
42
|
+
update_bytes_saved(source.size - destination.size)
|
43
|
+
say_status '%{source} (%{percent_change} / %{size_change} %{size_change_type})', Utils.file_size_stats(source, destination)
|
44
|
+
FileUtils.move(destination, source)
|
50
45
|
else
|
51
|
-
'
|
46
|
+
say_status '[skipped] %{source} not updated', source: source
|
52
47
|
end
|
48
|
+
ensure
|
49
|
+
ensure_file_mode(mode, source) unless mode.nil?
|
53
50
|
end
|
54
51
|
|
55
|
-
def
|
56
|
-
|
52
|
+
def updated_images
|
53
|
+
optimizable_images.select { |path| file_updated?(path) }
|
57
54
|
end
|
58
55
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
56
|
+
def optimizable_images
|
57
|
+
build_files.select do |path|
|
58
|
+
options.image_extensions.include?(File.extname(path)) && optimizer.optimizable?(path)
|
59
|
+
end
|
63
60
|
end
|
64
61
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
file_size_change_type = size_change_word(src_file.size, dst_file.size)
|
69
|
-
say_status "#{src_file} (#{file_percent_change} / #{file_size_change} #{file_size_change_type})"
|
62
|
+
def file_updated?(file_path)
|
63
|
+
return true unless options.manifest
|
64
|
+
File.mtime(file_path) != manifest.resource(file_path)
|
70
65
|
end
|
71
66
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
67
|
+
def preoptimize_modes(images)
|
68
|
+
images.inject({}) do |modes, image|
|
69
|
+
modes[image.to_s] = get_file_mode(image)
|
70
|
+
modes
|
75
71
|
end
|
76
72
|
end
|
77
73
|
|
74
|
+
def build_files
|
75
|
+
::Middleman::Util.all_files_under(app.build_dir)
|
76
|
+
end
|
77
|
+
|
78
|
+
def say_status(status, interpolations = {})
|
79
|
+
builder.say_status(:imageoptim, status % interpolations) if builder
|
80
|
+
end
|
81
|
+
|
78
82
|
def optimizer
|
79
|
-
@optimizer ||= ImageOptim.new(
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
units = %W(B KiB MiB GiB TiB)
|
94
|
-
|
95
|
-
if bytes.to_i < 1024
|
96
|
-
exponent = 0
|
97
|
-
else
|
98
|
-
max_exp = units.size - 1
|
99
|
-
exponent = (Math.log(bytes) / Math.log(1024)).to_i
|
100
|
-
exponent = max_exp if exponent > max_exp
|
101
|
-
bytes /= 1024 ** exponent
|
102
|
-
end
|
83
|
+
@optimizer ||= ImageOptim.new(options.imageoptim_options)
|
84
|
+
end
|
85
|
+
|
86
|
+
def manifest
|
87
|
+
@manifest ||= Manifest.new(app)
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_bytes_saved(bytes)
|
91
|
+
@byte_savings += bytes
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_file_mode(file)
|
95
|
+
sprintf('%o', File.stat(file).mode)[-4, 4].gsub(/^0*/, '')
|
96
|
+
end
|
103
97
|
|
104
|
-
|
98
|
+
def ensure_file_mode(mode, file)
|
99
|
+
return if mode == get_file_mode(file)
|
100
|
+
FileUtils.chmod(mode.to_i(8), file)
|
101
|
+
say_status 'fixed file mode on %{file} file to match source', file: file
|
105
102
|
end
|
106
103
|
end
|
107
104
|
end
|
@@ -1,64 +1,54 @@
|
|
1
1
|
module Middleman
|
2
2
|
module Imageoptim
|
3
|
-
|
4
3
|
# An options store that handles default options will accept user defined
|
5
4
|
# overrides
|
6
5
|
class Options
|
7
|
-
|
8
|
-
|
9
|
-
:
|
10
|
-
:
|
11
|
-
|
12
|
-
|
13
|
-
:
|
14
|
-
:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
!@user_options.jpegoptim_options.nil? ? @user_options.jpegoptim_options : {:strip => ['all'], :max_quality => 100}
|
54
|
-
end
|
55
|
-
|
56
|
-
def jpegtran_options
|
57
|
-
!@user_options.jpegtran_options.nil? ? @user_options.jpegtran_options : {:copy_chunks => false, :progressive => true, :jpegrescan => true}
|
58
|
-
end
|
59
|
-
|
60
|
-
def gifsicle_options
|
61
|
-
!@user_options.gifsicle_options.nil? ? @user_options.gifsicle_options : {:interlace => false}
|
6
|
+
# Mapping of valid option names to default values
|
7
|
+
EXTENSION_OPTIONS = [
|
8
|
+
:image_extensions,
|
9
|
+
:manifest
|
10
|
+
]
|
11
|
+
OPTIONS = {
|
12
|
+
advpng: { level: 4 },
|
13
|
+
allow_lossy: false,
|
14
|
+
gifsicle: { interlace: false },
|
15
|
+
image_extensions: %w(.png .jpg .jpeg .gif .svg),
|
16
|
+
jpegoptim: { strip: ['all'], max_quality: 100 },
|
17
|
+
jpegtran: { copy_chunks: false, progressive: true, jpegrescan: true },
|
18
|
+
nice: true,
|
19
|
+
manifest: true,
|
20
|
+
optipng: { level: 6, interlace: false },
|
21
|
+
pack: true,
|
22
|
+
pngcrush: { chunks: ['alla'], fix: false, brute: false },
|
23
|
+
pngout: { copy_chunks: false, strategy: 0 },
|
24
|
+
skip_missing_workers: true,
|
25
|
+
svgo: {},
|
26
|
+
threads: true,
|
27
|
+
verbose: false
|
28
|
+
}
|
29
|
+
|
30
|
+
attr_accessor *OPTIONS.keys.map(&:to_sym)
|
31
|
+
|
32
|
+
def initialize(user_options = {})
|
33
|
+
set_options(user_options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def imageoptim_options
|
37
|
+
Hash[instance_variables.map do |name|
|
38
|
+
[symbolize_key(name), instance_variable_get(name)]
|
39
|
+
end].reject { |key| EXTENSION_OPTIONS.include?(key) }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def symbolize_key(key)
|
45
|
+
key.to_s[1..-1].to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_options(user_options)
|
49
|
+
OPTIONS.keys.each do |name|
|
50
|
+
instance_variable_set(:"@#{name}", user_options.fetch(name, OPTIONS[name]))
|
51
|
+
end
|
62
52
|
end
|
63
53
|
end
|
64
54
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Middleman
|
2
|
+
module Imageoptim
|
3
|
+
class ResourceList
|
4
|
+
attr_reader :app, :options
|
5
|
+
|
6
|
+
def self.manipulate(app, resources, options)
|
7
|
+
new(app, resources, options).manipulate_resources
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(app, resources, options)
|
11
|
+
@app = app
|
12
|
+
@resources = resources
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def manipulate_resources
|
17
|
+
modified_resources << manifest_resource
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def modified_resources
|
23
|
+
@resources.map do |resource|
|
24
|
+
if resource_up_to_date?(resource)
|
25
|
+
Middleman::Sitemap::Resource.new(
|
26
|
+
app.sitemap,
|
27
|
+
resource.destination_path,
|
28
|
+
File.join(app.build_dir, resource.destination_path)
|
29
|
+
)
|
30
|
+
else
|
31
|
+
resource
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def manifest_resource
|
37
|
+
resource_args = [app.sitemap, Manifest::MANIFEST_FILENAME]
|
38
|
+
if File.exist?(File.join(app.build_dir, Manifest::MANIFEST_FILENAME))
|
39
|
+
resource_args << File.join(app.build_dir, Manifest::MANIFEST_FILENAME)
|
40
|
+
end
|
41
|
+
Middleman::Imageoptim::ManifestResource.new(*resource_args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_up_to_date?(resource)
|
45
|
+
image_resource?(resource) &&
|
46
|
+
sitemap_resource?(resource) &&
|
47
|
+
resource_exists?(resource) &&
|
48
|
+
resource_current?(resource)
|
49
|
+
end
|
50
|
+
|
51
|
+
def image_resource?(resource)
|
52
|
+
options.image_extensions.include?(File.extname(resource.destination_path))
|
53
|
+
end
|
54
|
+
|
55
|
+
def sitemap_resource?(resource)
|
56
|
+
resource.class.name == 'Middleman::Sitemap::Resource'
|
57
|
+
end
|
58
|
+
|
59
|
+
def resource_exists?(resource)
|
60
|
+
File.exist?(resource_build_path(resource))
|
61
|
+
end
|
62
|
+
|
63
|
+
def resource_current?(resource)
|
64
|
+
File.mtime(resource_build_path(resource)) > File.mtime(resource.source_file)
|
65
|
+
end
|
66
|
+
|
67
|
+
def resource_build_path(resource)
|
68
|
+
File.join(app.build_dir, resource.destination_path)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Middleman
|
2
|
+
module Imageoptim
|
3
|
+
module Utils
|
4
|
+
def self.size_change_word(size_a, size_b)
|
5
|
+
size_difference = (size_a - size_b)
|
6
|
+
if size_difference > 0
|
7
|
+
'smaller'
|
8
|
+
elsif size_difference < 0
|
9
|
+
'larger'
|
10
|
+
else
|
11
|
+
'no change'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.percentage_change(size_a, size_b)
|
16
|
+
'%.2f%%' % [100 - 100.0 * size_b / size_a]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.format_size(bytes)
|
20
|
+
units = %w(B KiB MiB GiB TiB)
|
21
|
+
if bytes.to_i < 1024
|
22
|
+
exponent = 0
|
23
|
+
else
|
24
|
+
max_exp = units.size - 1
|
25
|
+
exponent = (Math.log(bytes) / Math.log(1024)).to_i
|
26
|
+
exponent = max_exp if exponent > max_exp
|
27
|
+
bytes /= 1024**exponent
|
28
|
+
end
|
29
|
+
"#{bytes}#{units[exponent]}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.file_size_stats(source, destination)
|
33
|
+
{
|
34
|
+
source: source,
|
35
|
+
percent_change: percentage_change(source.size, destination.size),
|
36
|
+
size_change: format_size((source.size - destination.size)),
|
37
|
+
size_change_type: size_change_word(source.size, destination.size)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|