vitals_image 0.1.1 → 0.2.0

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: 39aa736f9e99bbf581d7e5b2f27df977921428c7aaf95846189475fd5ecc28ca
4
+ data.tar.gz: e879218f0859db14a8879ef761b88930758e73302d9b1e0e88e4fb7c7e3251a6
5
5
  SHA512:
6
- metadata.gz: ec023607a07b0910a34b0fc899d6e5dc8924382a2f1d96f6ceab26151360596e4fc4db7eff7c40c5201ab6f8ffc11c76abaef61e92ef305b4651476b8ef898ba
7
- data.tar.gz: 1fa954c88cf57d363c2e2a3695751fffaa38cf099642dd1bf32a026706105a4aab7e7f3d081278860c55c65b340ac061905388106b4ebc25b36e32b6ed01e21d
6
+ metadata.gz: e6a8f4819b849d25306338f00a3a3691e59acbe12417cfb88b5c5a722914b1d34bdfb792776e51a22f7d4efbed37030f8c35203ca8d4a362e8c0e4213d5776fa
7
+ data.tar.gz: 5058ad30a14d2edcb8ac744e5d8bb8130c66e0e874a3c088fb15ac297ba5911b8f9e95b19f7f67dbceb8f1a46201a2ed8f56c4b2e9271d7d19ad79476d637bab
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.2.0] - 2021-05-18
4
+
5
+ - Use the `optimial_quality` value of blob metadata instead of fixed `85` if its available;
6
+ - Remove some unecessary configs;
7
+ - Drop analyzers and use `active_analysis` gem instead;
8
+ - Use "retry once" strategy instead of `create_or_find_by`;
9
+ - Fix 'can't be referred' error;
10
+ - Do not cache development unless cache is enabled;
11
+
12
+ ## [0.1.1] - 2021-05-05
13
+
14
+ - Relax dependencies
15
+
16
+ ## [0.1.0] - 2021-05-01
17
+
18
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,202 @@
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
+ ```ruby
139
+ # jpeg_conversion
140
+ { sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 85, format: "jpg", background: :white, flatten: true, alpha: :off }
141
+
142
+ # jpeg_optimization:
143
+ { sampling_factor: "4:2:0", strip: true, interlace: "JPEG", colorspace: "sRGB", quality: 85 }
144
+
145
+ # png_optimization:
146
+ { strip: true, quality: 00 }
147
+ ```
148
+
149
+ These can be configured in your environment files, just like any other rails settings:
150
+
151
+ ```
152
+ Rails.application.configure do |config|
153
+ config.vitals_image.image_library = :vips
154
+ config.vitals_image.mobile_width = 410
155
+ config.vitals_image.desktop_width = 1264
156
+ config.vitals_image.lazy_loading = :lozad
157
+ config.vitals_image.require_alt_attribute = true
158
+ config.vitals_image.check_for_white_background = true
159
+ config.vitals_image.convert_to_jpeg = true
160
+ end
161
+ ```
162
+
163
+ 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.
164
+
165
+ ### Customization
166
+ 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.
167
+
168
+ ```ruby
169
+ class Base64ImageOptimizer < VitalsImage::Optimizer; end
170
+ class Base64ImageAnalyzer < VitalsImage::Analyzer; end
171
+ ```
172
+
173
+ And them during config add them ahead of others:
174
+ ```ruby
175
+ Rails.application.configure do |config|
176
+ config.vitals_image.optimizers.prepend Base64ImageOptimizer
177
+ config.vitals_image.analyzers.prepend Base64ImageAnalyzer
178
+ end
179
+ ```
180
+
181
+ ### TODO
182
+
183
+ - Check ACCEPT headers and serve modern file formats if possible (webp and avif)
184
+ - Add support for `srcset`
185
+
186
+ ## Development
187
+
188
+ 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.
189
+
190
+ 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).
191
+
192
+ ## Contributing
193
+
194
+ 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).
195
+
196
+ ## License
197
+
198
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
199
+
200
+ ## Code of Conduct
201
+
202
+ 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).
@@ -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,9 +9,10 @@ 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"
@@ -26,48 +27,55 @@ module VitalsImage
26
27
 
27
28
  config.vitals_image = ActiveSupport::OrderedOptions.new
28
29
  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.analyzers = [VitalsImage::Analyzer::UrlAnalyzer]
30
31
 
31
32
  config.eager_load_namespaces << VitalsImage
32
33
 
33
34
  initializer "vitals_image.configs" do
34
35
  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
36
+ VitalsImage.image_library = app.config.active_storage.variant_processor || :mini_magick
39
37
 
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
38
+ VitalsImage.logger = app.config.vitals_image.logger || Rails.logger
39
+ VitalsImage.optimizers = app.config.vitals_image.optimizers || []
40
+ VitalsImage.analyzers = app.config.vitals_image.analyzers || []
46
41
 
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
42
+ VitalsImage.mobile_width = app.config.vitals_image.mobile_width || :original
43
+ VitalsImage.desktop_width = app.config.vitals_image.desktop_width || :original
44
+ VitalsImage.resolution = app.config.vitals_image.resolution || 2
45
+ VitalsImage.lazy_loading = app.config.vitals_image.lazy_loading || :native
46
+ VitalsImage.lazy_loading_placeholder = app.config.vitals_image.lazy_loading_placeholder || VitalsImage::Base::TINY_GIF
47
+ VitalsImage.require_alt_attribute = app.config.vitals_image.require_alt_attribute || false
49
48
 
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
49
+ VitalsImage.check_for_white_background = app.config.vitals_image.check_for_white_background || true
55
50
 
56
- VitalsImage.skip_ssl_verification = app.config.vitals_image.skip_ssl_verification || false
51
+ VitalsImage.active_storage_route = app.config.vitals_image.active_storage_route || :inherited
52
+ VitalsImage.convert_to_jpeg = app.config.vitals_image.convert_to_jpeg || false
53
+ VitalsImage.jpeg_conversion = app.config.vitals_image.jpeg_conversion
54
+ VitalsImage.jpeg_optimization = app.config.vitals_image.jpeg_optimization
55
+ VitalsImage.png_optimization = app.config.vitals_image.png_optimization
56
+
57
+ VitalsImage.skip_ssl_verification = app.config.vitals_image.skip_ssl_verification || false
57
58
  end
58
59
  end
59
60
 
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
-
61
+ initializer "vitals_image.analyzers" do
64
62
  config.after_initialize do |app|
65
63
  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
64
+ app.config.active_analysis.addons << ActiveAnalysis::Addon::ImageAddon::WhiteBackground
65
+ end
66
+ end
67
+ end
68
+
69
+ initializer "vitals_image.optimizations" do
70
+ config.after_initialize do |app|
71
+ if VitalsImage.image_library == :vips
72
+ VitalsImage.jpeg_conversion ||= { saver: { strip: true, quality: 85, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3, background: 255 }, format: "jpg" }
73
+ VitalsImage.jpeg_optimization ||= { saver: { strip: true, quality: 85, interlace: true, optimize_coding: true, trellis_quant: true, quant_table: 3 } }
74
+ VitalsImage.png_optimization ||= { saver: { strip: true, compression: 9 } }
75
+ else
76
+ VitalsImage.jpeg_conversion ||= { saver: { strip: true, quality: 85, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB", background: :white, flatten: true, alpha: :off }, format: "jpg" }
77
+ VitalsImage.jpeg_optimization ||= { saver: { strip: true, quality: 85, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB" } }
78
+ VitalsImage.png_optimization ||= { saver: { strip: true, quality: 00 } }
71
79
  end
72
80
  end
73
81
  end
@@ -8,8 +8,8 @@ module VitalsImage
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 0
11
- MINOR = 1
12
- TINY = 1
11
+ MINOR = 2
12
+ TINY = 0
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -50,7 +50,7 @@ module VitalsImage
50
50
  end
51
51
 
52
52
  def resize_mode
53
- @options[:resize_mode] || @source.metadata["isolated"] ? :resize_and_pad : :resize_to_fill
53
+ @options[:resize_mode] || @source.metadata["white_background"] ? :resize_and_pad : :resize_to_fill
54
54
  end
55
55
 
56
56
  def variant
@@ -65,23 +65,32 @@ module VitalsImage
65
65
  end
66
66
 
67
67
  def optimize_jpeg
68
- @source.variant VitalsImage.jpeg_optimization.merge("#{resize_mode}": dimensions)
68
+ @source.variant optimizations_with_optimal_quality(VitalsImage.jpeg_optimization)
69
69
  end
70
70
 
71
71
  def optimize_png
72
72
  if alpha? || !VitalsImage.convert_to_jpeg
73
- @source.variant VitalsImage.png_optimization.merge("#{resize_mode}": dimensions)
73
+ @source.variant optimizations(VitalsImage.png_optimization)
74
74
  else
75
- @source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
75
+ @source.variant optimizations_with_optimal_quality(VitalsImage.jpeg_conversion)
76
76
  end
77
77
  end
78
78
 
79
79
  def optimize_generic
80
80
  if alpha? || !VitalsImage.convert_to_jpeg
81
- @source.variant("#{resize_mode}": dimensions)
81
+ @source.variant optimizations
82
82
  else
83
- @source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
83
+ @source.variant optimizations_with_optimal_quality(VitalsImage.jpeg_conversion)
84
84
  end
85
85
  end
86
+
87
+ def optimizations_with_optimal_quality(defaults = {})
88
+ quality = @source.metadata[:optimal_quality] || defaults[:saver][:quality]
89
+ defaults.merge quality: quality, "#{resize_mode}": dimensions
90
+ end
91
+
92
+ def optimizations(defaults = {})
93
+ defaults.merge "#{resize_mode}": dimensions
94
+ end
86
95
  end
87
96
  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.2.0
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-06-18 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.3'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0.3'
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,11 +246,9 @@ 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
@@ -248,6 +262,7 @@ homepage: https://github.com/FestaLab/vitals_image
248
262
  licenses:
249
263
  - MIT
250
264
  metadata:
265
+ allowed_push_host: https://rubygems.org
251
266
  homepage_uri: https://github.com/FestaLab/vitals_image
252
267
  source_code_uri: https://github.com/FestaLab/vitals_image
253
268
  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