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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +202 -0
- data/app/models/vitals_image/source.rb +1 -1
- data/lib/vitals_image.rb +0 -1
- data/lib/vitals_image/analyzer/{url.rb → url_analyzer.rb} +1 -1
- data/lib/vitals_image/cache.rb +18 -7
- data/lib/vitals_image/engine.rb +37 -29
- data/lib/vitals_image/gem_version.rb +2 -2
- data/lib/vitals_image/optimizer/active_storage.rb +15 -6
- metadata +20 -5
- data/lib/vitals_image/core_extensions/active_storage/image_analyzer.rb +0 -59
- data/lib/vitals_image/core_extensions/active_storage/isolated_image_analyzer.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39aa736f9e99bbf581d7e5b2f27df977921428c7aaf95846189475fd5ecc28ca
|
4
|
+
data.tar.gz: e879218f0859db14a8879ef761b88930758e73302d9b1e0e88e4fb7c7e3251a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 <
|
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
data/lib/vitals_image/cache.rb
CHANGED
@@ -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
|
-
|
14
|
+
with_retry do
|
15
|
+
source = @store.read(key)
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
data/lib/vitals_image/engine.rb
CHANGED
@@ -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/
|
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::
|
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.
|
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.
|
41
|
-
VitalsImage.
|
42
|
-
VitalsImage.
|
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.
|
48
|
-
VitalsImage.
|
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.
|
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.
|
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.
|
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.
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
@@ -50,7 +50,7 @@ module VitalsImage
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def resize_mode
|
53
|
-
@options[:resize_mode] || @source.metadata["
|
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
|
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
|
73
|
+
@source.variant optimizations(VitalsImage.png_optimization)
|
74
74
|
else
|
75
|
-
@source.variant VitalsImage.jpeg_conversion
|
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
|
81
|
+
@source.variant optimizations
|
82
82
|
else
|
83
|
-
@source.variant VitalsImage.jpeg_conversion
|
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.
|
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-
|
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/
|
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
|