middleman-imageoptim 0.1.4 → 0.2.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.
- 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
|