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.
@@ -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