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.
@@ -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 "image_optim"
4
- require "fileutils"
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
- def initialize(app, builder, options)
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
- images_to_optimize = filter_file_paths(file_paths())
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 filter_file_paths(paths)
31
- paths.select {|path|
32
- is_image_extension(path.extname) && image_is_optimizable(path)
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 is_image_extension(extension)
37
- @options.image_extensions.include?(extension)
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
- def image_is_optimizable(path)
41
- optimizer.optimizable?(path)
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 size_change_word(size_src, size_dst)
45
- size_difference = (size_src - size_dst)
46
- if size_difference > 0
47
- 'smaller'
48
- elsif size_difference < 0
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
- 'no change'
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 percentage_change(size_src, size_dst)
56
- '%.2f%%' % [100 - 100.0 * size_dst / size_src]
52
+ def updated_images
53
+ optimizable_images.select { |path| file_updated?(path) }
57
54
  end
58
55
 
59
- private
60
-
61
- def file_paths
62
- ::Middleman::Util.all_files_under(@app.inst.build_dir)
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 say_file_size_stats(src_file, dst_file)
66
- file_percent_change = percentage_change(src_file.size, dst_file.size)
67
- file_size_change = format_size((src_file.size - dst_file.size))
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 say_status(status)
73
- if @builder
74
- @builder.say_status :image_optim, status
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
- :nice => @options.nice,
81
- :threads => @options.threads,
82
- :pngcrush => @options.pngcrush_options,
83
- :pngout => @options.pngout_options,
84
- :optipng => @options.optipng_options,
85
- :advpng => @options.advpng_options,
86
- :jpegoptim => @options.jpegoptim_options,
87
- :jpegtran => @options.jpegtran_options,
88
- :gifsicle => @options.gifsicle_options
89
- )
90
- end
91
-
92
- def format_size(bytes)
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
- "#{bytes}#{units[exponent]}"
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
- attr_accessor :user_options
8
- attr_reader :verbose, :nice, :threads, :image_extensions,
9
- :pngcrush_options, :pngout_options, :optipng_options, :advpng_options,
10
- :jpegoptim_options, :jpegtran_options, :gifsicle_options
11
-
12
- UserOptions = Struct.new(:verbose, :nice, :threads, :image_extensions,
13
- :pngcrush_options, :pngout_options, :optipng_options, :advpng_options,
14
- :jpegoptim_options, :jpegtran_options, :gifsicle_options)
15
-
16
- def initialize(options_hash = {})
17
- @user_options = UserOptions.new(*options_hash)
18
- end
19
-
20
- def verbose
21
- !@user_options.verbose.nil? ? @user_options.verbose : false
22
- end
23
-
24
- def nice
25
- !@user_options.nice.nil? ? @user_options.nice : true
26
- end
27
-
28
- def threads
29
- !@user_options.threads.nil? ? @user_options.threads : true
30
- end
31
-
32
- def image_extensions
33
- !@user_options.image_extensions.nil? ? @user_options.image_extensions : %w(.png .jpg .gif)
34
- end
35
-
36
- def pngcrush_options
37
- !@user_options.pngcrush_options.nil? ? @user_options.pngcrush_options : {:chunks => ['alla'], :fix => false, :brute => false}
38
- end
39
-
40
- def pngout_options
41
- !@user_options.pngout_options.nil? ? @user_options.pngout_options : {:copy_chunks => false, :strategy => 0}
42
- end
43
-
44
- def optipng_options
45
- !@user_options.optipng_options.nil? ? @user_options.optipng_options : {:level => 6, :interlace => false}
46
- end
47
-
48
- def advpng_options
49
- !@user_options.advpng_options.nil? ? @user_options.advpng_options : {:level => 4}
50
- end
51
-
52
- def jpegoptim_options
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
@@ -1,6 +1,6 @@
1
1
  module Middleman
2
2
  module Imageoptim
3
3
  PACKAGE = 'middleman-imageoptim'
4
- VERSION = "0.1.4"
4
+ VERSION = '0.2.0'
5
5
  end
6
6
  end