middleman-simple-thumbnailer 1.0.2 → 1.4.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.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.devcontainer/Dockerfile +20 -0
  3. data/.devcontainer/devcontainer.json +35 -0
  4. data/.github/workflows/ci.yml +22 -0
  5. data/.gitignore +4 -0
  6. data/README.md +84 -12
  7. data/config/cucumber.yml +1 -0
  8. data/features/caching_thumbnails_in_development.feature +29 -1
  9. data/features/generate_image_thumbnails.feature +19 -5
  10. data/features/generate_image_thumbnails_image_path.feature +44 -0
  11. data/features/generate_image_thumbnails_relative_assets.feature +43 -0
  12. data/features/generate_new_data_file.feature +21 -0
  13. data/features/given_data_file.feature +30 -0
  14. data/features/support/image_thumbnails_steps.rb +89 -11
  15. data/fixtures/basic-app-data-file-generate/config.rb +1 -0
  16. data/fixtures/basic-app-data-file-generate/source/images/original.jpg +0 -0
  17. data/fixtures/basic-app-data-file-generate/source/page-with-images-to-resize.html.erb +10 -0
  18. data/fixtures/basic-app-data-file-generate/source/page-with-untouched-image.html.erb +9 -0
  19. data/fixtures/basic-app-data-file/config.rb +1 -0
  20. data/fixtures/basic-app-data-file/data/simple_thumbnailer.yml +5 -0
  21. data/fixtures/basic-app-data-file/source/images/original.jpg +0 -0
  22. data/fixtures/basic-app-data-file/source/page-with-images-to-resize.html.erb +10 -0
  23. data/fixtures/basic-app-data-file/source/page-with-untouched-image.html.erb +9 -0
  24. data/fixtures/basic-app-data-file/source/sitemap.html.erb +5 -0
  25. data/fixtures/basic-app-image-path-use-cache/config.rb +1 -0
  26. data/fixtures/basic-app-image-path-use-cache/source/images/original.jpg +0 -0
  27. data/fixtures/basic-app-image-path-use-cache/source/page-with-images-to-resize.html.erb +16 -0
  28. data/fixtures/basic-app-image-path-use-cache/source/page-with-untouched-image.html.erb +12 -0
  29. data/fixtures/basic-app-image-path/config.rb +1 -0
  30. data/fixtures/basic-app-image-path/source/images/original.jpg +0 -0
  31. data/fixtures/basic-app-image-path/source/page-with-images-to-resize.html.erb +16 -0
  32. data/fixtures/basic-app-image-path/source/page-with-untouched-image.html.erb +12 -0
  33. data/fixtures/basic-app-no-resize/config.rb +1 -0
  34. data/fixtures/basic-app-no-resize/source/images/original.jpg +0 -0
  35. data/fixtures/basic-app-no-resize/source/page-with-untouched-image.html.erb +9 -0
  36. data/fixtures/basic-app-relative-assets-use-cache/config.rb +2 -0
  37. data/fixtures/basic-app-relative-assets-use-cache/source/images/original.jpg +0 -0
  38. data/fixtures/basic-app-relative-assets-use-cache/source/page-with-images-to-resize.html.erb +16 -0
  39. data/fixtures/basic-app-relative-assets-use-cache/source/page-with-untouched-image.html.erb +9 -0
  40. data/fixtures/basic-app-relative-assets/config.rb +2 -0
  41. data/fixtures/basic-app-relative-assets/source/images/original.jpg +0 -0
  42. data/fixtures/basic-app-relative-assets/source/page-with-images-to-resize.html.erb +16 -0
  43. data/fixtures/basic-app-relative-assets/source/page-with-untouched-image.html.erb +9 -0
  44. data/fixtures/basic-app-use-cache/config.rb +1 -0
  45. data/fixtures/basic-app-use-cache/source/images/original.jpg +0 -0
  46. data/fixtures/basic-app-use-cache/source/page-with-images-to-resize.html.erb +10 -0
  47. data/fixtures/basic-app-use-cache/source/page-with-untouched-image.html.erb +9 -0
  48. data/fixtures/basic-app/source/page-with-images-to-resize.html.erb +2 -2
  49. data/fixtures/basic-app/source/page-with-untouched-image.html.erb +1 -1
  50. data/fixtures/cached-thumbnails-use-cache/config.rb +1 -0
  51. data/fixtures/cached-thumbnails-use-cache/source/images/original.jpg +0 -0
  52. data/fixtures/cached-thumbnails-use-cache/source/page-with-images-to-resize.html.erb +10 -0
  53. data/fixtures/custom-cache-dir-use-cache/config.rb +3 -0
  54. data/fixtures/custom-cache-dir-use-cache/source/images/original.jpg +0 -0
  55. data/fixtures/custom-cache-dir-use-cache/source/page-with-images-to-resize.html.erb +10 -0
  56. data/lib/middleman-simple-thumbnailer.rb +4 -1
  57. data/lib/middleman-simple-thumbnailer/extension.rb +143 -17
  58. data/lib/middleman-simple-thumbnailer/image.rb +46 -37
  59. data/lib/middleman-simple-thumbnailer/image_store.rb +42 -0
  60. data/lib/middleman-simple-thumbnailer/rack.rb +42 -0
  61. data/lib/middleman-simple-thumbnailer/resource.rb +19 -0
  62. data/lib/middleman-simple-thumbnailer/version.rb +1 -1
  63. data/middleman-simple-thumbnailer.gemspec +8 -6
  64. metadata +104 -45
  65. data/.travis.yml +0 -10
@@ -2,4 +2,7 @@ require 'mini_magick'
2
2
  require "middleman-core"
3
3
  require "middleman-simple-thumbnailer/version"
4
4
  require "middleman-simple-thumbnailer/image"
5
- require "middleman-simple-thumbnailer/extension"
5
+ require "middleman-simple-thumbnailer/image_store"
6
+ require "middleman-simple-thumbnailer/extension"
7
+ require "middleman-simple-thumbnailer/resource"
8
+ require "middleman-simple-thumbnailer/rack"
@@ -1,5 +1,9 @@
1
- #
2
- # add resize_to param to image_tag to create thumbnails
1
+ require 'tmpdir'
2
+ require 'yaml'
3
+ require 'json'
4
+ require 'tempfile'
5
+ require 'uri'
6
+ # add resize_to param to image_tag to create thumbnails
3
7
  #
4
8
  #
5
9
  # Usage:
@@ -9,37 +13,159 @@ module MiddlemanSimpleThumbnailer
9
13
  class Extension < Middleman::Extension
10
14
 
11
15
  option :cache_dir, 'tmp/simple-thumbnailer-cache', 'Directory (relative to project root) for cached thumbnails.'
16
+ option :use_specs, false, 'Wether or not use the specification data file'
17
+ option :specs_data, 'simple_thumbnailer', 'name of the specification data file. Must follow middleman data file name convention'
18
+ option :specs_data_default_format, 'yaml', 'defaut specification file format (and extension). Can be yaml, yml or json'
19
+ option :specs_data_save_old, true, 'save previous specification data file '
20
+ option :update_specs, true, 'Warn about missing image file in specification and add them to teh spec file'
21
+ option :use_cache_dev, false, 'In development, add a Rack middleware to serve the resized image from cache'
12
22
 
13
23
  def initialize(app, options_hash={}, &block)
14
- super
15
- app.after_build do |builder|
16
- MiddlemanSimpleThumbnailer::Image.all_objects.each do |image|
17
- builder.say_status :create, "#{image.resized_img_path}"
18
- image.save!
24
+ super
25
+ @images_store = MiddlemanSimpleThumbnailer::ImageStore.new
26
+ @resize_specs = app.data[options.specs_data] || []
27
+ if @resize_specs.length == 0
28
+ @resize_specs = []
29
+ end
30
+ if !%w(yaml yml json).include?(options.specs_data_default_format)
31
+ raise "value assigned to option specs_data_default_format is not correct. should be one of json, yaml, or yml"
19
32
  end
20
- end
21
33
  end
22
34
 
23
35
  def after_configuration
24
- MiddlemanSimpleThumbnailer::Image.options = options
36
+ if app.development? && options.use_cache_dev
37
+ app.use MiddlemanSimpleThumbnailer::Rack, options, app, options
38
+ end
39
+ end
40
+
41
+ def store_resized_image(img_path, resize_to)
42
+ @images_store.store(img_path, resize_to)
43
+ end
44
+
45
+ def check_image_in_specs(img_path, resize_to)
46
+ @resize_specs.each do |resize_spec|
47
+ if resize_to == resize_spec.resize_to && File.fnmatch(resize_spec.path, img_path)
48
+ return true
49
+ end
50
+ end
51
+ return false
52
+ end
53
+
54
+ def manipulate_resource_list(resources)
55
+ return resources unless options.use_specs
56
+ resources + @resize_specs.reduce([]) do |res, resize_spec|
57
+ Dir.chdir(File.absolute_path(File.join(app.root, app.config[:source], app.config[:images_dir]))) do
58
+ Dir.glob(resize_spec.path) do |image_file|
59
+ store_resized_image(image_file, resize_spec.resize_to)
60
+ img = MiddlemanSimpleThumbnailer::Image.new(image_file, resize_spec.resize_to, app, options).tap do |i|
61
+ i.prepare_thumbnail
62
+ end
63
+ resized_image_path = File.join(app.config[:images_dir],img.resized_img_path)
64
+ new_resource = MiddlemanSimpleThumbnailer::Resource.new(
65
+ app.sitemap,
66
+ resized_image_path,
67
+ img.cached_resized_img_abs_path,
68
+ )
69
+ res.push(new_resource)
70
+ end
71
+ end
72
+ res
73
+ end
74
+ end
75
+
76
+ def after_build(builder)
77
+ resize_specs_modified = false
78
+ @images_store.each do |img_path, resize_to|
79
+ if options.use_specs && options.update_specs && !check_image_in_specs(img_path, resize_to)
80
+ builder.thor.say_status :warning, "#{img_path}:#{resize_to} not in resize spec", :yellow
81
+ @resize_specs.push(Middleman::Util::EnhancedHash.new({'path': img_path, 'resize_to': resize_to}))
82
+ resize_specs_modified = true
83
+ end
84
+ img = MiddlemanSimpleThumbnailer::Image.new(img_path, resize_to, builder.app, options)
85
+ if !File.exists?(img.resized_img_abs_path)
86
+ builder.thor.say_status :create, "#{img.resized_img_abs_path}"
87
+ img.save!
88
+ end
89
+ end
90
+ @images_store.delete
91
+ if options.update_specs && resize_specs_modified
92
+ builder.thor.say_status :warning, "Resize specs modified, rewrite", :yellow
93
+ write_specs_file
94
+ end
95
+ end
96
+
97
+ def get_data_file_path_ext
98
+ specs_data_list = Dir.glob(File.absolute_path(File.join(app.root, app.config[:data_dir],"#{options.specs_data}.{json,yml,yaml}")))
99
+ if specs_data_list.length > 0
100
+ return specs_data_list[0], File.extname(specs_data_list[0])[1..-1]
101
+ else
102
+ return File.absolute_path(File.join(app.root, app.config[:data_dir],"#{options.specs_data}.#{options.specs_data_default_format}")), options.specs_data_default_format
103
+ end
25
104
  end
26
105
 
106
+ def write_specs_file
107
+ data_file, ext = get_data_file_path_ext
108
+ FileUtils.mkdir_p File::dirname(data_file)
109
+ if File.exists?(data_file) && options.specs_data_save_old
110
+ i = 1
111
+ old_data_file = "#{data_file}.#{i}"
112
+ while File.exists?(old_data_file) && (i+=1) < ((2**16)-1)
113
+ old_data_file = "#{data_file}.#{i}"
114
+ end
115
+ raise "Middleman-simple-thumbnailer : could not find a filename for saving the data file " if i == ((2**16)-1)
116
+ FileUtils.cp(data_file, old_data_file)
117
+ end
118
+ new_specs = @resize_specs.map do |resize_spec|
119
+ resize_spec.to_hash
120
+ end
121
+ File.open(data_file, 'w') {|f| f.write ((%w(yaml yml).include?(ext))?new_specs.to_yaml():JSON.pretty_generate(new_specs)) }
122
+ end
123
+
124
+
125
+ private :check_image_in_specs, :get_data_file_path_ext, :write_specs_file
126
+
27
127
  helpers do
28
128
 
29
- def image_tag(path, options={})
30
- resize_to = options.delete(:resize_to)
31
- return super(path, options) unless resize_to
129
+ def resized_image_path(path, resize_to=nil)
130
+ return path unless resize_to
32
131
 
33
- image = MiddlemanSimpleThumbnailer::Image.new(path, resize_to, self.config)
34
- if environment == :development
35
- super("data:#{image.mime_type};base64,#{image.base64_data}", options)
132
+ ext = app.extensions[:middleman_simple_thumbnailer]
133
+
134
+ image = MiddlemanSimpleThumbnailer::Image.new(path, resize_to, app, ext.options)
135
+ if app.development?
136
+ if ext.options.use_cache_dev
137
+ [path, {"simple-thumbnailer" => "#{path}|#{resize_to}"}]
138
+ else
139
+ ["data:#{image.mime_type};base64,#{image.base64_data}", {}]
140
+ end
36
141
  else
37
- super(image.resized_img_path, options)
142
+ ext.store_resized_image(path, resize_to)
143
+ [image.resized_img_path, {}]
144
+ end
145
+ end
146
+
147
+ def image_tag(path, options={})
148
+ resize_to = options.delete(:resize_to)
149
+ new_path, query_params = resize_to ? resized_image_path(path, resize_to) : [path, {}]
150
+ if(!query_params.empty?)
151
+ new_path = "#{new_path}?#{URI.encode_www_form(query_params)}"
38
152
  end
153
+ super(new_path, options)
39
154
  end
40
155
 
156
+ def image_path(path, options={})
157
+ resize_to = options.delete(:resize_to)
158
+ new_path, query_params = resize_to ? resized_image_path(path, resize_to) : [path, {}]
159
+ res = super(new_path)
160
+ if(!query_params.empty?)
161
+ res = "#{res}?#{URI.encode_www_form(query_params)}"
162
+ end
163
+ res
164
+ end
165
+
41
166
  end
167
+
42
168
  end
43
169
  end
44
170
 
45
- ::Middleman::Extensions.register(:middleman_simple_thumbnailer, MiddlemanSimpleThumbnailer::Extension)
171
+ ::Middleman::Extensions.register(:middleman_simple_thumbnailer, MiddlemanSimpleThumbnailer::Extension)
@@ -1,18 +1,17 @@
1
- require "digest"
1
+ require 'digest'
2
+ require 'base64'
2
3
 
3
4
  module MiddlemanSimpleThumbnailer
4
5
  class Image
5
- @@all_objects = []
6
- @@options = nil
7
6
 
8
7
  attr_accessor :img_path, :middleman_config, :resize_to
9
8
 
10
- def initialize(img_path, resize_to, middleman_config)
11
- @@all_objects << self
12
-
9
+ def initialize(img_path, resize_to, app, options_hash)
13
10
  @img_path = img_path
14
11
  @resize_to = resize_to
15
- @middleman_config = middleman_config
12
+ @middleman_config = app.config
13
+ @app = app
14
+ @options = options_hash
16
15
  end
17
16
 
18
17
  def mime_type
@@ -24,29 +23,48 @@ module MiddlemanSimpleThumbnailer
24
23
  end
25
24
 
26
25
  def base64_data
26
+ prepare_thumbnail
27
+ Base64.strict_encode64(File.read(cached_resized_img_abs_path))
28
+ end
29
+
30
+ def prepare_thumbnail
27
31
  unless cached_thumbnail_available?
28
32
  resize!
29
33
  save_cached_thumbnail
30
34
  end
31
- Base64.encode64(File.read(cached_resized_img_abs_path))
32
35
  end
33
36
 
37
+ def render
38
+ prepare_thumbnail
39
+ File.read(cached_resized_img_abs_path)
40
+ end
41
+
42
+
34
43
  def save!
35
- resize!
36
- image.write(resized_img_abs_path)
44
+ prepare_thumbnail
45
+ FileUtils.copy_file(cached_resized_img_abs_path, resized_img_abs_path)
46
+ end
47
+
48
+ def resized_img_abs_path
49
+ File.join(build_dir, middleman_abs_path).gsub(image_name, resized_image_name)
37
50
  end
38
51
 
39
- def self.all_objects
40
- @@all_objects
52
+ def middleman_resized_abs_path
53
+ middleman_abs_path.gsub(image_name, resized_image_name)
41
54
  end
42
55
 
43
- def self.options=(options)
44
- @@options = options
56
+ def middleman_abs_path
57
+ img_path.start_with?('/') ? img_path : File.join(images_dir, img_path)
45
58
  end
46
-
47
-
59
+
60
+ def cached_resized_img_abs_path
61
+ File.join(cache_dir, middleman_abs_path).gsub(image_name, resized_image_name).split('.').tap { |a|
62
+ a.insert(-2, image_checksum)
63
+ }.join('.')
64
+ end
65
+
48
66
  private
49
-
67
+
50
68
  def resize!
51
69
  unless @already_resized
52
70
  image.resize(resize_to)
@@ -66,40 +84,31 @@ module MiddlemanSimpleThumbnailer
66
84
  File.basename(abs_path)
67
85
  end
68
86
 
87
+ def original_mtime
88
+ File.mtime abs_path
89
+ end
90
+
69
91
  def resized_image_name
70
92
  image_name.split('.').tap { |a| a.insert(-2, resize_to) }.join('.') # add resize_to sufix
71
- .gsub(/[%@!<>^]/, '>' => 'gt', '<' => 'lt', '^' => 'c') # sanitize file name
93
+ .gsub(/[%@!<>^]/, '>' => 'gt', '<' => 'lt', '^' => 'c') # sanitize file name
72
94
  end
73
95
 
74
96
  def abs_path
75
97
  File.join(source_dir, middleman_abs_path)
76
98
  end
77
99
 
78
- def middleman_abs_path
79
- img_path.start_with?('/') ? img_path : File.join(images_dir, img_path)
80
- end
81
-
82
- def resized_img_abs_path
83
- File.join(build_dir, middleman_abs_path).gsub(image_name, resized_image_name)
84
- end
85
-
86
- def cached_resized_img_abs_path
87
- File.join(cache_dir, middleman_abs_path).gsub(image_name, resized_image_name).split('.').tap { |a|
88
- a.insert(-2, image_checksum)
89
- }.join('.')
90
- end
91
-
92
100
  def cached_thumbnail_available?
93
101
  File.exist?(cached_resized_img_abs_path)
94
102
  end
95
-
103
+
96
104
  def save_cached_thumbnail
97
105
  FileUtils.mkdir_p(File.dirname(cached_resized_img_abs_path))
98
106
  image.write(cached_resized_img_abs_path)
107
+ File.utime(original_mtime, original_mtime, cached_resized_img_abs_path)
99
108
  end
100
-
109
+
101
110
  def source_dir
102
- middleman_config[:source]
111
+ File.absolute_path(middleman_config[:source], @app.root)
103
112
  end
104
113
 
105
114
  def images_dir
@@ -111,7 +120,7 @@ module MiddlemanSimpleThumbnailer
111
120
  end
112
121
 
113
122
  def cache_dir
114
- @@options.cache_dir
123
+ File.absolute_path(@options.cache_dir, @app.root)
115
124
  end
116
125
  end
117
- end
126
+ end
@@ -0,0 +1,42 @@
1
+ module MiddlemanSimpleThumbnailer
2
+
3
+ class ImageStore
4
+
5
+ attr_reader :tmp_path
6
+
7
+ def initialize
8
+ @tmp_path = Dir::Tmpname.create('thumbnail', nil) {}
9
+ end
10
+
11
+ def store(img_path, resize_to)
12
+ File.open(@tmp_path, File::RDWR|File::CREAT, 0644) do |f|
13
+ f.flock(File::LOCK_EX)
14
+ resized_images = f.size > 0 ? Marshal.load(f) : {}
15
+ file_key = "#{img_path}.#{resize_to}"
16
+ if ! resized_images.has_key?(file_key)
17
+ resized_images[file_key] = [img_path, resize_to]
18
+ f.rewind
19
+ Marshal.dump(resized_images,f)
20
+ f.flush
21
+ f.truncate(f.pos)
22
+ end
23
+ end
24
+ end
25
+
26
+ def each
27
+ return unless File.exist?(@tmp_path)
28
+ File.open(@tmp_path, "r") do |f|
29
+ f.flock(File::LOCK_SH)
30
+ resized_images = f.size > 0 ? Marshal.load(f) : {}
31
+ resized_images.values.each do |store_entry|
32
+ yield *store_entry
33
+ end
34
+ end
35
+ end
36
+
37
+ def delete
38
+ File.delete(@tmp_path) if File.exist?(@tmp_path)
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ require 'rack/utils'
2
+ require 'cgi'
3
+
4
+ module MiddlemanSimpleThumbnailer
5
+ class Rack
6
+ def initialize(app, options= {}, middleman_app, config)
7
+ @app = app
8
+ @options = options
9
+ @config = config
10
+ @middleman_app = middleman_app
11
+ end
12
+
13
+ def call(env)
14
+ path_info = env["PATH_INFO"]
15
+ query_str = env["QUERY_STRING"]
16
+ environment = @middleman_app.config[:environment]
17
+ status = -1
18
+
19
+ if(environment === :development && !query_str.empty?)
20
+ query_hash = CGI::parse(query_str)
21
+ if(query_hash.key?('simple-thumbnailer'))
22
+ path, resize_to = query_hash['simple-thumbnailer'][0].split('|')
23
+ image = MiddlemanSimpleThumbnailer::Image.new(path, resize_to, @middleman_app, @config)
24
+ status = 200
25
+ file_data = image.render
26
+ headers = {
27
+ "Content-Length" => file_data.bytesize.to_s,
28
+ "Content-Type" => image.mime_type
29
+ }
30
+ response = [file_data]
31
+ end
32
+ end
33
+
34
+ if status === -1
35
+ status, headers, response = @app.call(env)
36
+ end
37
+
38
+ [status, headers, response]
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module MiddlemanSimpleThumbnailer
2
+ class Resource < ::Middleman::Sitemap::Resource
3
+ def initialize(store, path, source)
4
+ super(store, path, source)
5
+ end
6
+
7
+ def ignored?
8
+ false
9
+ end
10
+
11
+ def template?
12
+ false
13
+ end
14
+
15
+ def binary?
16
+ true
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module MiddlemanSimpleThumbnailer
2
- VERSION = '1.0.2'
2
+ VERSION = '1.4.0'
3
3
  end