vitals_image 0.1.1 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d8a81b62c6510bb742ceb8eecf2ab2fd37a5d07d53e2c9f9736bef089e6134d
4
- data.tar.gz: e1590ed0d07ddcb7120950f3b2049cdcb6aa5a6774fee11e37e3be3e557dfc86
3
+ metadata.gz: 5399352c9f774e90f9a671459d582672c69c19b5640675e47a5c77536cb36826
4
+ data.tar.gz: a05b961a9730225fd60f81c8d5467ab44a542abd8fa96d659891f805ee5a21b7
5
5
  SHA512:
6
- metadata.gz: ec023607a07b0910a34b0fc899d6e5dc8924382a2f1d96f6ceab26151360596e4fc4db7eff7c40c5201ab6f8ffc11c76abaef61e92ef305b4651476b8ef898ba
7
- data.tar.gz: 1fa954c88cf57d363c2e2a3695751fffaa38cf099642dd1bf32a026706105a4aab7e7f3d081278860c55c65b340ac061905388106b4ebc25b36e32b6ed01e21d
6
+ metadata.gz: 41a2aca3f4efb2178525af7566d24fe9825f858860a0b482c48ada2f49bc62edadb085986f165dc7d1d2875c5faf2e60114040c6318c6f52e469db620b32fb4d
7
+ data.tar.gz: 1fe231165885de9b8c0876abfc466583f9fcb0da7c5ea2da73104e0d93348064c55e41fe9471decc0c077ac8392efcfe3f5c49b75db305c743752c7d78eeaa9a
data/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.4.1] - 2021-07-22
4
+
5
+ - Do not discard the specified `style` attribute
6
+
7
+ ## [0.4.0] - 2021-07-06
8
+
9
+ - Fixed resize and pad in vips
10
+ - Fixed image quality in vips
11
+ - Reduced JPEG quality from 85 to 80
12
+ - Removed optimal quality
13
+ - Updated Vitals Image to 0.5
14
+
15
+ ## [0.3.0] - 2021-05-22
16
+
17
+ - Redo the image helper
18
+ - Redo the active storage optimizer
19
+
20
+ ## [0.2.1] - 2021-05-22
21
+
22
+ - Do not attempt to transform invariable images
23
+
24
+ ## [0.2.0] - 2021-05-18
25
+
26
+ - Use the `optimal_quality` value of blob metadata instead of fixed `85` if its available;
27
+ - Remove some unecessary configs;
28
+ - Drop analyzers and use `active_analysis` gem instead;
29
+ - Use "retry once" strategy instead of `create_or_find_by`;
30
+ - Fix 'can't be referred' error;
31
+ - Do not cache development unless cache is enabled;
32
+
33
+ ## [0.1.1] - 2021-05-05
34
+
35
+ - Relax dependencies
36
+
37
+ ## [0.1.0] - 2021-05-01
38
+
39
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # Vitals Image
2
+
3
+ Vitals Image is a lib that makes it easier to create image tags that follow best practices for fast loading web pages in rails projects. It does that by adding a new view helper (`vitals_image_tag`) that can take a string or an active storage attachment and automatically set width, height and a few other recommended attributes.
4
+
5
+ This gem was extracted from FestaLab's app and replaced the original code ([see it in action](https://festalab.com.br/modelo-de-convite?referer=github)).
6
+
7
+ [![vitals-image-main](https://github.com/FestaLab/vitals_image/actions/workflows/main.yml/badge.svg)](https://github.com/FestaLab/vitals_image/actions/workflows/main.yml)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'vitals_image'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Finally copy over the migration:
22
+
23
+ $ bin/rails vitals_image:install:migrations
24
+
25
+ ## Usage
26
+
27
+ ### Basics
28
+ The simplest usage for `vitals_image` is to replace a normal `image_tag` with it.
29
+
30
+ Before:
31
+ ```rhtml
32
+ <%= image_tag "icon.svg" %>
33
+ <img src="icon.svg" />
34
+ ```
35
+
36
+ After:
37
+ ```rhtml
38
+ <%= vitals_image_tag "icon.svg" %>
39
+
40
+ <!-- First time the image is seem -->
41
+ <img src="icon.svg" loading="lazy" decoding="async" />
42
+
43
+ <!-- Second time the image is seem -->
44
+ <img src="icon.svg" width="20" height="40" loading="lazy" decoding="async" style="height: auto;" />
45
+ ```
46
+
47
+ If the supplied source is an external url, or the url of an asset, `vitals_image` will, in a background job, download the image, analyze it and store its width and height, so that the next time it is seem, they can be applied, reducing the page's CLS. As for the extra tags:
48
+
49
+ - `loading`: setting it to `lazy` allows Chrome to lazy load images that are not in the viewport without the need for a lazy load library.
50
+ - `decoding`: another optimization similar to `loading`, which allows image to the decoded asynchronously.
51
+ - `style`: setting `height: auto` allows your CSS to choose a different `width` for your image (`width: 100%;`) while keeping its aspect ratio intact and still benefitting from no impact on the CLS
52
+
53
+ The same can be done with an active storage image:
54
+
55
+ Before:
56
+ ```rhtml
57
+ <%= image_tag user.avatar %>
58
+ <img src="/rails/active_storage/blobs/redirect/(...).photo.jpeg" />
59
+ ```
60
+
61
+ After:
62
+ ```rhtml
63
+ <%= vitals_image_tag user.avatar %>
64
+
65
+ <!-- Before active storage has analyzed the image -->
66
+ <img src="/rails/active_storage/blobs/redirect/(...).photo.jpeg" loading="lazy" decoding="async" />
67
+
68
+ <!-- After active storage has analyzed the image -->
69
+ <img src="/rails/active_storage/blobs/redirect/(...).photo.jpeg" width="200" height="200" loading="lazy" decoding="async" style="height: auto;" />
70
+ ```
71
+
72
+ ### Setting width and height
73
+ You might however decide to use a different width or height for your images. No problem, give one dimension, and `vitals_image` will figure out the other.
74
+ ```rhtml
75
+ <%= vitals_image_tag "icon.svg", width: 10 %>
76
+ <img src="icon.svg" width="10" height="20" loading="lazy" decoding="async" style="height: auto;" />
77
+ ```
78
+
79
+ If you do the same in active storage, instead of the original image, you will get an optimized variant:
80
+ ```rhtml
81
+ <%= vitals_image_tag user.avatar width: 100 %>
82
+ <img src="/rails/active_storage/representations/redirect/(...).photo.jpeg" width="100" height="100" loading="lazy" decoding="async" style="height: auto;" />
83
+ ```
84
+
85
+ To see which optimizations will be applied (and change them if you wish, check the "Configuration" section.)
86
+
87
+ If you set both a width and a height and end up with a different aspect ratio than the image has, Vitals Image will make a "best guess" at what you want.
88
+
89
+ For an image from an url, which it cannot apply transformations to, it will use `object-fit`
90
+ ```rhtml
91
+ <%= vitals_image_tag "icon.svg", width: 100, height: 40 %>
92
+ <img src="icon.svg" width="100" height="40" style="object-fit: contain" />
93
+ ```
94
+
95
+ For an active storage image, it has two possible strategies for the resize:
96
+
97
+ - `resize_to_limit`: This is the default. Downsizes the image to fit within the specified dimensions while retaining the original aspect ratio. Will only resize the image if it's larger than the specified dimensions.
98
+ - `resize_and_pad`: This only be used if Vitals Image know that this image is an object in a white background. For that to work you must set `image_library = :vips` and `check_for_white_background = true` in your configuration. This will cause Vitals Image to replace the normal `analyze_job` that Active Storage uses, with a custom one that will add the attribute `white_background` to the blobs metadata,
99
+
100
+ ### Advanced options
101
+ You can disable lazy loading if you want:
102
+ ```rhtml
103
+ <%= vitals_image_tag "icon.svg", lazy_load: false %>
104
+ <img src="icon.svg" width="20" height="40" style="height: auto;" />
105
+ ```
106
+
107
+ You can also choose a different route strategy for active storage, than the one it uses by default:
108
+ ```rhtml
109
+ <%= vitals_image_tag user.avatar, active_storage_route: :proxy %>
110
+ <img src="/rails/active_storage/representations/proxy/(...).photo.jpeg" width="100" height="100" loading="lazy" decoding="async" style="height: auto;" />
111
+ ```
112
+
113
+ If you set the height, you will not get the `auto` added to it:
114
+ ```rhtml
115
+ <%= vitals_image_tag "icon.svg", height: 20 %>
116
+ <img src="icon.svg" width="10" height="20" />
117
+ ```
118
+
119
+
120
+
121
+ ## Configuration
122
+ The following configuration options are available. The defaults were chosen for maximum compatibility and least surprises, while the values under "Recommended" are what the app from which this gem was extracted uses.
123
+
124
+ | Options | Default | Recommended | Description |
125
+ | --------------------------------|----------------|-------------------------|-------------|
126
+ | image_library | `:mini_magick` | `:vips` | The image library that will be used to analyze and optimize images. While `mini_magick` is available in most PaaS and CIs, `vips` is faster and uses less resources. |
127
+ | resolution | `2` | `2` | The resolution that downsized images will have. While some phones are capable of `3` or `4`, `2` should be [good enough for most people](https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html) |
128
+ | lazy_loading | `:native` | `:lozad` or `:lazyload` | If left at `:native`, the tags `loading` and `decoding` will be added. Otherwise, the value of this option will be added to the `class` attribute and `src` moved to `data-src`. I recommend either [lozad](https://apoorv.pro/lozad.js/) or [lazysizes](https://github.com/aFarkas/lazysizes)
129
+ | lazy_loading_placeholder | Blank GIF | Blank GIF | When using a lazy load library, this image will be used as the placeholder in the `src` attribute so that users don't see a broken image. See it [here](https://github.com/FestaLab/vitals_image/blob/main/lib/vitals_image/engine.rb#L36). |
130
+ | require_alt_attribute | `false` | `true` | Will raise in exception if the `alt` attribute is not supplied to the helper. Useful to ensure no one ever forgets it again. |
131
+ | check_for_white_background | `false` | `true` | Requires `image_library = :vips`. Same as above, but the analyzer will also try to deduce if the image is a photo, or a product on a white background. This will help define if `resize_to_limit` or `resize_and_pad` should be used when you supply both `width` and `height` to the helper. |
132
+ | convert_to_jpeg | `false` | `true` | If set to `true`, images will be converted to JPEG, unless the keyword `alpha: true` was used in the helper. |
133
+ | jpeg_conversion | see below | see below | Hash of options to pass to active storage when converting other image formats to JPEG and optimizing. |
134
+ | jpeg_optimization | see below | see below | Hash of options to pass to active storage when optimizing a JPEG. |
135
+ | png_optimization | see below | see below | Hash of options to pass to active storage when optimizing a PNG. |
136
+ | active_storage_route | `:inherited` | `:inherited` | Defines how urls of active storage images will be generated. If `inherited` it will use the same as active storage. Other valid options are `redirect`, `proxy` and `public`. Whatever is set here can be overriden in the helper. |
137
+
138
+ Minimagick
139
+ ```ruby
140
+ # jpeg_conversion
141
+ { saver: { strip: true, quality: 85, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB", background: :white, flatten: true, alpha: :off }, format: "jpg" }
142
+
143
+ # jpeg_optimization:
144
+ { saver: { strip: true, quality: 85, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB" } }
145
+
146
+ # png_optimization:
147
+ { saver: { strip: true, quality: 00 } }
148
+ ```
149
+
150
+ Vips
151
+ ```ruby
152
+ # jpeg_conversion
153
+ { saver: { strip: true, quality: 85, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3, background: 255 }, format: "jpg" }
154
+
155
+ # jpeg_optimization:
156
+ { saver: { strip: true, quality: 85, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3 } }
157
+
158
+ # png_optimization:
159
+ { saver: { strip: true, compression: 9 } }
160
+ ```
161
+
162
+ These can be configured in your environment files, just like any other rails settings:
163
+
164
+ ```
165
+ Rails.application.configure do |config|
166
+ config.vitals_image.image_library = :vips
167
+ config.vitals_image.mobile_width = 410
168
+ config.vitals_image.desktop_width = 1264
169
+ config.vitals_image.lazy_loading = :lozad
170
+ config.vitals_image.require_alt_attribute = true
171
+ config.vitals_image.check_for_white_background = true
172
+ config.vitals_image.convert_to_jpeg = true
173
+ end
174
+ ```
175
+
176
+ Heads up! If you are already on Rails 7.0.0.alpha, make sure you are not using `image_decoding` and `image_loading` config options below, as they will forcibly add native lazy loading and async decoding to the tags.
177
+
178
+ ### Customization
179
+ If your use case requires it, you can easily add extra `optimizers` and `analyzers`, just like you would in Active Storage. Start by inheriting the abstract classes.
180
+
181
+ ```ruby
182
+ class Base64ImageOptimizer < VitalsImage::Optimizer; end
183
+ class Base64ImageAnalyzer < VitalsImage::Analyzer; end
184
+ ```
185
+
186
+ And them during config add them ahead of others:
187
+ ```ruby
188
+ Rails.application.configure do |config|
189
+ config.vitals_image.optimizers.prepend Base64ImageOptimizer
190
+ config.vitals_image.analyzers.prepend Base64ImageAnalyzer
191
+ end
192
+ ```
193
+
194
+ ### TODO
195
+
196
+ - Check ACCEPT headers and serve modern file formats if possible (webp and avif)
197
+ - Add support for `srcset`
198
+
199
+ ## Development
200
+
201
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
202
+
203
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
204
+
205
+ ## Contributing
206
+
207
+ Bug reports and pull requests are welcome on GitHub at https://github.com/FestaLab/vitals_image. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/FestaLab/vitals_image/blob/main/CODE_OF_CONDUCT.md).
208
+
209
+ ## License
210
+
211
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
212
+
213
+ ## Code of Conduct
214
+
215
+ Everyone interacting in the VitalsImage project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/FestaLab/vitals_image/blob/main/CODE_OF_CONDUCT.md).
@@ -6,32 +6,17 @@ module VitalsImage
6
6
  source = image_url(source) if source.is_a?(String)
7
7
  optimizer = VitalsImage::Base.optimizer(source, options)
8
8
 
9
- if !optimizer.variable?
10
- vitals_image_invariable_tag(optimizer)
11
- elsif optimizer.native_lazy_load?
12
- vitals_image_variable_tag(optimizer)
13
- else
14
- vitals_image_lazy_tag(optimizer)
15
- end
16
- end
17
-
18
- private
19
- def vitals_image_invariable_tag(optimizer)
20
- image_tag optimizer.src, optimizer.html_options
21
- end
22
-
23
- def vitals_image_variable_tag(optimizer)
24
- url = vitals_image_url(optimizer.src, optimizer.html_options)
25
- image_tag url, optimizer.html_options
26
- end
27
-
28
- def vitals_image_lazy_tag(optimizer)
9
+ if optimizer.non_native_lazy_load?
29
10
  url = vitals_image_url(optimizer.html_options["data"]["src"], optimizer.html_options)
30
11
  optimizer.html_options["data"]["src"] = url
31
-
32
12
  image_tag optimizer.src, optimizer.html_options
13
+ else
14
+ url = vitals_image_url(optimizer.src, optimizer.html_options)
15
+ image_tag url, optimizer.html_options
33
16
  end
17
+ end
34
18
 
19
+ private
35
20
  def vitals_image_url(source, options)
36
21
  active_storage_route = options.delete("active_storage_route") || VitalsImage.active_storage_route
37
22
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VitalsImage
4
- class Source < ApplicationRecord
4
+ class Source < ActiveRecord::Base
5
5
  store :metadata, accessors: [ :analyzed, :width, :height ], coder: ActiveRecord::Coders::JSON, default: "{ analyzed: false }"
6
6
 
7
7
  after_create -> { AnalyzeJob.perform_later(self) }
data/lib/vitals_image.rb CHANGED
@@ -21,7 +21,6 @@ module VitalsImage
21
21
  mattr_accessor :lazy_loading_placeholder
22
22
  mattr_accessor :require_alt_attribute
23
23
 
24
- mattr_accessor :replace_active_storage_analyzer
25
24
  mattr_accessor :check_for_white_background
26
25
 
27
26
  mattr_accessor :convert_to_jpeg
@@ -3,7 +3,7 @@
3
3
  require "open-uri"
4
4
 
5
5
  module VitalsImage
6
- class Analyzer::Url < Analyzer
6
+ class Analyzer::UrlAnalyzer < Analyzer
7
7
  def self.accept?(source)
8
8
  source.is_a?(Source)
9
9
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../app/models/vitals_image/source"
4
+
3
5
  module VitalsImage
4
6
  class Cache
5
7
  include Singleton
@@ -9,15 +11,24 @@ module VitalsImage
9
11
  end
10
12
 
11
13
  def locate(key)
12
- source = @store.read(key)
14
+ with_retry do
15
+ source = @store.read(key)
13
16
 
14
- if source.blank?
15
- source = Source.create_or_find_by(key: key)
16
- expires_in = source.analyzed ? nil : 1.minute
17
- @store.write(key, source, expires_in: expires_in)
18
- end
17
+ if source.blank?
18
+ source = Source.find_or_create_by(key: key)
19
+ expires_in = source.analyzed ? nil : 1.minute
20
+ @store.write(key, source, expires_in: expires_in)
21
+ end
19
22
 
20
- source
23
+ source
24
+ end
21
25
  end
26
+
27
+ private
28
+ def with_retry
29
+ yield
30
+ rescue ActiveRecord::RecordNotUnique
31
+ yield
32
+ end
22
33
  end
23
34
  end
@@ -9,65 +9,74 @@ require "active_support"
9
9
  require "marcel"
10
10
  require "ruby-vips"
11
11
  require "mini_magick"
12
+ require "active_analysis"
12
13
 
13
14
  require "vitals_image/analyzer"
14
- require "vitals_image/analyzer/url"
15
+ require "vitals_image/analyzer/url_analyzer"
15
16
  require "vitals_image/base"
16
17
  require "vitals_image/cache"
17
18
  require "vitals_image/errors"
18
19
  require "vitals_image/optimizer"
19
20
  require "vitals_image/optimizer/blank"
20
21
  require "vitals_image/optimizer/url"
21
- require "vitals_image/optimizer/active_storage"
22
+ require "vitals_image/optimizer/variable"
23
+ require "vitals_image/optimizer/invariable"
22
24
 
23
25
  module VitalsImage
24
26
  class Engine < ::Rails::Engine
25
27
  isolate_namespace VitalsImage
26
28
 
27
29
  config.vitals_image = ActiveSupport::OrderedOptions.new
28
- config.vitals_image.optimizers = [VitalsImage::Optimizer::Blank, VitalsImage::Optimizer::ActiveStorage, VitalsImage::Optimizer::Url]
29
- config.vitals_image.analyzers = [VitalsImage::Analyzer::Url]
30
+ config.vitals_image.optimizers = [VitalsImage::Optimizer::Blank, VitalsImage::Optimizer::Variable, VitalsImage::Optimizer::Invariable, VitalsImage::Optimizer::Url]
31
+ config.vitals_image.analyzers = [VitalsImage::Analyzer::UrlAnalyzer]
30
32
 
31
33
  config.eager_load_namespaces << VitalsImage
32
34
 
33
35
  initializer "vitals_image.configs" do
34
36
  config.after_initialize do |app|
35
- VitalsImage.logger = app.config.vitals_image.logger || Rails.logger
36
- VitalsImage.optimizers = app.config.vitals_image.optimizers || []
37
- VitalsImage.analyzers = app.config.vitals_image.analyzers || []
38
- VitalsImage.image_library = app.config.vitals_image.image_library || :mini_magick
37
+ VitalsImage.image_library = app.config.active_storage.variant_processor || :mini_magick
39
38
 
40
- VitalsImage.mobile_width = app.config.vitals_image.mobile_width || :original
41
- VitalsImage.desktop_width = app.config.vitals_image.desktop_width || :original
42
- VitalsImage.resolution = app.config.vitals_image.resolution || 2
43
- VitalsImage.lazy_loading = app.config.vitals_image.lazy_loading || :native
44
- VitalsImage.lazy_loading_placeholder = app.config.vitals_image.lazy_loading_placeholder || VitalsImage::Base::TINY_GIF
45
- VitalsImage.require_alt_attribute = app.config.vitals_image.require_alt_attribute || false
39
+ VitalsImage.logger = app.config.vitals_image.logger || Rails.logger
40
+ VitalsImage.optimizers = app.config.vitals_image.optimizers || []
41
+ VitalsImage.analyzers = app.config.vitals_image.analyzers || []
46
42
 
47
- VitalsImage.replace_active_storage_analyzer = app.config.vitals_image.replace_active_storage_analyzer || false
48
- VitalsImage.check_for_white_background = app.config.vitals_image.check_for_white_background || false
43
+ VitalsImage.mobile_width = app.config.vitals_image.mobile_width || :original
44
+ VitalsImage.desktop_width = app.config.vitals_image.desktop_width || :original
45
+ VitalsImage.resolution = app.config.vitals_image.resolution || 2
46
+ VitalsImage.lazy_loading = app.config.vitals_image.lazy_loading || :native
47
+ VitalsImage.lazy_loading_placeholder = app.config.vitals_image.lazy_loading_placeholder || VitalsImage::Base::TINY_GIF
48
+ VitalsImage.require_alt_attribute = app.config.vitals_image.require_alt_attribute || false
49
49
 
50
- VitalsImage.convert_to_jpeg = app.config.vitals_image.convert_to_jpeg || false
51
- VitalsImage.jpeg_conversion = app.config.vitals_image.jpeg_conversion || { sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80, format: "jpg", background: :white, flatten: true, alpha: :off }
52
- VitalsImage.jpeg_optimization = app.config.vitals_image.jpeg_optimization || { sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 80 }
53
- VitalsImage.png_optimization = app.config.vitals_image.png_optimization || { strip: true, quality: 00 }
54
- VitalsImage.active_storage_route = app.config.vitals_image.png_optimization || :inherited
50
+ VitalsImage.check_for_white_background = app.config.vitals_image.check_for_white_background || true
55
51
 
56
- VitalsImage.skip_ssl_verification = app.config.vitals_image.skip_ssl_verification || false
52
+ VitalsImage.active_storage_route = app.config.vitals_image.active_storage_route || :inherited
53
+ VitalsImage.convert_to_jpeg = app.config.vitals_image.convert_to_jpeg || false
54
+ VitalsImage.jpeg_conversion = app.config.vitals_image.jpeg_conversion
55
+ VitalsImage.jpeg_optimization = app.config.vitals_image.jpeg_optimization
56
+ VitalsImage.png_optimization = app.config.vitals_image.png_optimization
57
+
58
+ VitalsImage.skip_ssl_verification = app.config.vitals_image.skip_ssl_verification || false
57
59
  end
58
60
  end
59
61
 
60
- initializer "vitals_image.core_extensions" do
61
- require_relative "core_extensions/active_storage/image_analyzer"
62
- require_relative "core_extensions/active_storage/isolated_image_analyzer"
63
-
62
+ initializer "vitals_image.analyzers" do
64
63
  config.after_initialize do |app|
65
64
  if VitalsImage.check_for_white_background
66
- app.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
67
- app.config.active_storage.analyzers.prepend CoreExtensions::ActiveStorage::IsolatedImageAnalyzer
68
- elsif VitalsImage.replace_active_storage_analyzer
69
- app.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
70
- app.config.active_storage.analyzers.prepend CoreExtensions::ActiveStorage::ImageAnalyzer
65
+ app.config.active_analysis.addons << ActiveAnalysis::Addon::ImageAddon::WhiteBackground
66
+ end
67
+ end
68
+ end
69
+
70
+ initializer "vitals_image.optimizations" do
71
+ config.after_initialize do |app|
72
+ if VitalsImage.image_library == :vips
73
+ VitalsImage.jpeg_conversion ||= { saver: { strip: true, quality: 80, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3, background: 255 }, format: "jpg" }
74
+ VitalsImage.jpeg_optimization ||= { saver: { strip: true, quality: 80, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3 } }
75
+ VitalsImage.png_optimization ||= { saver: { strip: true, compression: 9 } }
76
+ else
77
+ VitalsImage.jpeg_conversion ||= { saver: { strip: true, quality: 80, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB", background: :white, flatten: true, alpha: :off }, format: "jpg" }
78
+ VitalsImage.jpeg_optimization ||= { saver: { strip: true, quality: 80, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB" } }
79
+ VitalsImage.png_optimization ||= { saver: { strip: true, quality: 75 } }
71
80
  end
72
81
  end
73
82
  end
@@ -8,7 +8,7 @@ module VitalsImage
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 0
11
- MINOR = 1
11
+ MINOR = 4
12
12
  TINY = 1
13
13
  PRE = nil
14
14
 
@@ -26,10 +26,10 @@ module VitalsImage
26
26
 
27
27
  def html_options
28
28
  @html_options ||= begin
29
- html_options = @options.dup
29
+ html_options = @options.dup.except("lazy_load")
30
30
  html_options["width"] = width
31
31
  html_options["height"] = height
32
- html_options["style"] = style
32
+ html_options["style"] = "#{style} #{html_options["style"]}".squish.presence
33
33
  html_options["class"] = "vitals-image #{html_options["class"]}".squish
34
34
 
35
35
  if non_native_lazy_load?
@@ -57,11 +57,6 @@ module VitalsImage
57
57
  lazy_load? && VitalsImage.lazy_loading == :native
58
58
  end
59
59
 
60
- # Override this method in a concrete subclass. Have it return true the source is an active storage blob
61
- def variable?
62
- false
63
- end
64
-
65
60
  private
66
61
  def style
67
62
  if analyzed? && !requested_height
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VitalsImage
4
+ class Optimizer::Invariable < Optimizer
5
+ def self.accept?(source)
6
+ source.respond_to?(:variable?) && !source.variable?
7
+ end
8
+
9
+ private
10
+ def source_url
11
+ @source
12
+ end
13
+
14
+ def style
15
+ if !analyzed?
16
+ # Do nothing
17
+ elsif !requested_height
18
+ "height:auto;"
19
+ elsif fixed_dimensions?
20
+ "object-fit: contain;"
21
+ end
22
+ end
23
+
24
+ def original_width
25
+ metadata[:width]
26
+ end
27
+
28
+ def original_height
29
+ metadata[:height]
30
+ end
31
+
32
+ def metadata
33
+ @source.metadata
34
+ end
35
+
36
+ def analyzed?
37
+ metadata[:analyzed]
38
+ end
39
+ end
40
+ end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VitalsImage
4
- class Optimizer::ActiveStorage < Optimizer
4
+ class Optimizer::Variable < Optimizer
5
5
  def self.accept?(source)
6
- source.is_a?(::ActiveStorage::Attached) || source.is_a?(::ActiveStorage::Attachment) || source.is_a?(::ActiveStorage::Blob)
7
- end
8
-
9
- def variable?
10
- true
6
+ source.respond_to?(:variable?) && source.variable?
11
7
  end
12
8
 
13
9
  private
@@ -50,7 +46,7 @@ module VitalsImage
50
46
  end
51
47
 
52
48
  def resize_mode
53
- @options[:resize_mode] || @source.metadata["isolated"] ? :resize_and_pad : :resize_to_fill
49
+ @options[:resize_mode] || @source.metadata["white_background"] ? :resize_and_pad : :resize_to_fill
54
50
  end
55
51
 
56
52
  def variant
@@ -65,23 +61,32 @@ module VitalsImage
65
61
  end
66
62
 
67
63
  def optimize_jpeg
68
- @source.variant VitalsImage.jpeg_optimization.merge("#{resize_mode}": dimensions)
64
+ @source.variant resize_and_flatten(VitalsImage.jpeg_optimization)
69
65
  end
70
66
 
71
67
  def optimize_png
72
68
  if alpha? || !VitalsImage.convert_to_jpeg
73
- @source.variant VitalsImage.png_optimization.merge("#{resize_mode}": dimensions)
69
+ @source.variant resize(VitalsImage.png_optimization)
74
70
  else
75
- @source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
71
+ @source.variant resize_and_flatten(VitalsImage.jpeg_conversion)
76
72
  end
77
73
  end
78
74
 
79
75
  def optimize_generic
80
76
  if alpha? || !VitalsImage.convert_to_jpeg
81
- @source.variant("#{resize_mode}": dimensions)
77
+ @source.variant resize
82
78
  else
83
- @source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
79
+ @source.variant resize_and_flatten(VitalsImage.jpeg_conversion)
84
80
  end
85
81
  end
82
+
83
+ def resize_and_flatten(defaults = {})
84
+ resize = resize_mode != :resize_and_pad ? dimensions : dimensions.push(background: [255])
85
+ defaults.merge "#{resize_mode}": resize
86
+ end
87
+
88
+ def resize(defaults = {})
89
+ defaults.merge "#{resize_mode}": dimensions
90
+ end
86
91
  end
87
92
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vitals_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Breno Gazzola
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-06 00:00:00.000000000 Z
11
+ date: 2021-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '1.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: active_analysis
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0.5'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0.5'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: sqlite3
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -213,7 +227,9 @@ executables: []
213
227
  extensions: []
214
228
  extra_rdoc_files: []
215
229
  files:
230
+ - CHANGELOG.md
216
231
  - MIT-LICENSE
232
+ - README.md
217
233
  - Rakefile
218
234
  - app/assets/config/vitals_image_manifest.js
219
235
  - app/assets/stylesheets/vitals_image/application.css
@@ -230,24 +246,24 @@ files:
230
246
  - lib/tasks/vitals_image_tasks.rake
231
247
  - lib/vitals_image.rb
232
248
  - lib/vitals_image/analyzer.rb
233
- - lib/vitals_image/analyzer/url.rb
249
+ - lib/vitals_image/analyzer/url_analyzer.rb
234
250
  - lib/vitals_image/base.rb
235
251
  - lib/vitals_image/cache.rb
236
- - lib/vitals_image/core_extensions/active_storage/image_analyzer.rb
237
- - lib/vitals_image/core_extensions/active_storage/isolated_image_analyzer.rb
238
252
  - lib/vitals_image/engine.rb
239
253
  - lib/vitals_image/errors.rb
240
254
  - lib/vitals_image/gem_version.rb
241
255
  - lib/vitals_image/optimizer.rb
242
- - lib/vitals_image/optimizer/active_storage.rb
243
256
  - lib/vitals_image/optimizer/blank.rb
257
+ - lib/vitals_image/optimizer/invariable.rb
244
258
  - lib/vitals_image/optimizer/url.rb
259
+ - lib/vitals_image/optimizer/variable.rb
245
260
  - lib/vitals_image/test_case.rb
246
261
  - lib/vitals_image/version.rb
247
262
  homepage: https://github.com/FestaLab/vitals_image
248
263
  licenses:
249
264
  - MIT
250
265
  metadata:
266
+ allowed_push_host: https://rubygems.org
251
267
  homepage_uri: https://github.com/FestaLab/vitals_image
252
268
  source_code_uri: https://github.com/FestaLab/vitals_image
253
269
  changelog_uri: https://github.com/FestaLab/vitals_image/CHANGELOG.mg
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VitalsImage
4
- module CoreExtensions
5
- module ActiveStorage
6
- class ImageAnalyzer < ::ActiveStorage::Analyzer
7
- def self.accept?(blob)
8
- blob.image?
9
- end
10
-
11
- def metadata
12
- read_image do |image|
13
- if rotated_image?(image)
14
- { width: image.height, height: image.width }
15
- else
16
- { width: image.width, height: image.height }
17
- end
18
- end
19
- end
20
-
21
- private
22
- def read_image
23
- download_blob_to_tempfile do |file|
24
- require "ruby-vips"
25
- image = Vips::Image.new_from_file(file.path, access: :sequential)
26
-
27
- if valid_image?(image)
28
- yield image
29
- else
30
- logger.info "Skipping image analysis because Vips doesn't support the file"
31
- {}
32
- end
33
- end
34
- rescue LoadError
35
- logger.info "Skipping image analysis because the ruby-vips gem isn't installed"
36
- {}
37
- rescue Vips::Error => error
38
- logger.error "Skipping image analysis due to an Vips error: #{error.message}"
39
- {}
40
- end
41
-
42
- ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
43
-
44
- def rotated_image?(image)
45
- ROTATIONS === image.get("exif-ifd0-Orientation")
46
- rescue ::Vips::Error
47
- false
48
- end
49
-
50
- def valid_image?(image)
51
- image.avg
52
- true
53
- rescue ::Vips::Error
54
- false
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VitalsImage
4
- module CoreExtensions
5
- module ActiveStorage
6
- class IsolatedImageAnalyzer < ImageAnalyzer
7
- def metadata
8
- read_image do |image|
9
- if rotated_image?(image)
10
- { width: image.height, height: image.width, isolated: isolated?(image) }
11
- else
12
- { width: image.width, height: image.height, isolated: isolated?(image) }
13
- end
14
- end
15
- end
16
-
17
- private
18
- def isolated?(image)
19
- corners = extract_corner_areas(image)
20
- colors = corners.map { |corner| primary_color_for(corner) }
21
- colors.all? { |color| color.all? { |value| value > 250 } }
22
- rescue
23
- false
24
- end
25
-
26
- def extract_corner_areas(image)
27
- paths = []
28
-
29
- basename = SecureRandom.urlsafe_base64
30
- width = image.width
31
- height = image.height
32
- size = 8
33
-
34
- filename = Rails.root.join("tmp", "#{basename}.jpg")
35
- `vips copy #{image.filename} #{filename}`
36
-
37
- paths << Rails.root.join("tmp", "#{basename}_top_left.jpg")
38
- `vips im_extract_area #{filename} #{paths.last} 0 0 #{size} #{size}`
39
-
40
- paths << Rails.root.join("tmp", "#{basename}_top_right.jpg")
41
- `vips im_extract_area #{filename} #{paths.last} #{width - size} 0 #{size} #{size}`
42
-
43
- paths << Rails.root.join("tmp", "#{basename}_bottom_right.jpg")
44
- `vips im_extract_area #{filename} #{paths.last} #{width - size} #{height - size} #{size} #{size}`
45
-
46
- paths << Rails.root.join("tmp", "#{basename}_bottom_left.jpg")
47
- `vips im_extract_area #{filename} #{paths.last} 0 #{height - size} #{size} #{size}`
48
-
49
- paths
50
- end
51
-
52
- def primary_color_for(filepath)
53
- histogram = generate_color_histogram(filepath)
54
- sorted = sort_by_frequency(histogram)
55
- extract_dominant_rgb(sorted)
56
- end
57
-
58
- def generate_color_histogram(path)
59
- `convert #{path} +dither -colors 5 -define histogram:unique-colors=true -format "%c" histogram:info:`
60
- end
61
-
62
- def sort_by_frequency(histogram)
63
- histogram.each_line.map { |line| parts = line.split(":"); [parts[0].to_i, parts[1]] }.sort_by { |line| line[0] }.reverse
64
- end
65
-
66
- def extract_dominant_rgb(array)
67
- array.map { |line| line[1].match(/\(([\d.,]+)/).captures.first.split(",").take(3).map(&:to_i) }.first
68
- end
69
- end
70
- end
71
- end
72
- end