vitals_image 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +19 -0
- data/app/assets/config/vitals_image_manifest.js +1 -0
- data/app/assets/stylesheets/vitals_image/application.css +15 -0
- data/app/controllers/vitals_image/application_controller.rb +6 -0
- data/app/helpers/vitals_image/application_helper.rb +6 -0
- data/app/helpers/vitals_image/tag_helper.rb +50 -0
- data/app/jobs/vitals_image/analyze_job.rb +11 -0
- data/app/jobs/vitals_image/application_job.rb +6 -0
- data/app/models/vitals_image/application_record.rb +7 -0
- data/app/models/vitals_image/source.rb +9 -0
- data/app/views/layouts/vitals_image/application.html.erb +15 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20210502132155_create_vitals_image_sources.rb +15 -0
- data/lib/tasks/vitals_image_tasks.rake +5 -0
- data/lib/vitals_image.rb +34 -0
- data/lib/vitals_image/analyzer.rb +22 -0
- data/lib/vitals_image/analyzer/url.rb +54 -0
- data/lib/vitals_image/base.rb +21 -0
- data/lib/vitals_image/cache.rb +23 -0
- data/lib/vitals_image/core_extensions/active_storage/image_analyzer.rb +59 -0
- data/lib/vitals_image/core_extensions/active_storage/isolated_image_analyzer.rb +72 -0
- data/lib/vitals_image/engine.rb +81 -0
- data/lib/vitals_image/errors.rb +15 -0
- data/lib/vitals_image/gem_version.rb +17 -0
- data/lib/vitals_image/optimizer.rb +135 -0
- data/lib/vitals_image/optimizer/active_storage.rb +87 -0
- data/lib/vitals_image/optimizer/blank.rb +30 -0
- data/lib/vitals_image/optimizer/url.rb +45 -0
- data/lib/vitals_image/test_case.rb +9 -0
- data/lib/vitals_image/version.rb +10 -0
- metadata +273 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 84a3011e3da96900b519bb6ce6055c82fcbcca849afc9848d3a1d997132a5b32
|
4
|
+
data.tar.gz: 744defafc94c3ec847f170fd77b38e714ff3b6c1e9960eb921b3464ae8726efd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0554f9fa24c1e86a49732d56ca4521df497986e45e246bf94bc278ed23ffa582968def6122627bfd8e949eef6845ab20d69ec837dbc892fd75464e5e8e250afd
|
7
|
+
data.tar.gz: 2234ac3485c84b894794ca313b4e5ca18d7ff2c9f0c7efbb5133bbda95684fc5ce81c0d28ef61c902a5c36a9ae2f85b5b9945bf12c5d61328fff01fe992fa6e6
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 Breno Gazzola
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
6
|
+
|
7
|
+
load "rails/tasks/engine.rake"
|
8
|
+
load "rails/tasks/statistics.rake"
|
9
|
+
|
10
|
+
require "bundler/gem_tasks"
|
11
|
+
require "rake/testtask"
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
14
|
+
t.libs << "test"
|
15
|
+
t.pattern = "test/**/*_test.rb"
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: :test
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/vitals_image .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
module TagHelper
|
5
|
+
def vitals_image_tag(source, options = {})
|
6
|
+
source = image_url(source) if source.is_a?(String)
|
7
|
+
optimizer = VitalsImage::Base.optimizer(source, options)
|
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)
|
29
|
+
url = vitals_image_url(optimizer.html_options["data"]["src"], optimizer.html_options)
|
30
|
+
optimizer.html_options["data"]["src"] = url
|
31
|
+
|
32
|
+
image_tag optimizer.src, optimizer.html_options
|
33
|
+
end
|
34
|
+
|
35
|
+
def vitals_image_url(source, options)
|
36
|
+
active_storage_route = options.delete("active_storage_route") || VitalsImage.active_storage_route
|
37
|
+
|
38
|
+
case active_storage_route
|
39
|
+
when :redirect
|
40
|
+
rails_storage_redirect_path(source)
|
41
|
+
when :proxy
|
42
|
+
rails_storage_proxy_path(source)
|
43
|
+
when :public
|
44
|
+
source.is_a?(ActiveStorage::VariantWithRecord) ? source.processed.url : source.url
|
45
|
+
else
|
46
|
+
url_for(source)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Source < ApplicationRecord
|
5
|
+
store :metadata, accessors: [ :analyzed, :width, :height ], coder: ActiveRecord::Coders::JSON, default: "{ analyzed: false }"
|
6
|
+
|
7
|
+
after_create -> { AnalyzeJob.perform_later(self) }
|
8
|
+
end
|
9
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateVitalsImageSources < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
create_table :vitals_image_sources do |t|
|
6
|
+
t.string :key, null: false
|
7
|
+
t.string :content_type
|
8
|
+
t.text :metadata
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
|
12
|
+
t.index [ :key ], unique: true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/vitals_image.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "vitals_image/version"
|
4
|
+
require "vitals_image/engine"
|
5
|
+
|
6
|
+
module VitalsImage
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :FixtureSet
|
10
|
+
|
11
|
+
|
12
|
+
mattr_accessor :logger
|
13
|
+
mattr_accessor :optimizers
|
14
|
+
mattr_accessor :analyzers
|
15
|
+
mattr_accessor :image_library
|
16
|
+
|
17
|
+
mattr_accessor :mobile_width
|
18
|
+
mattr_accessor :desktop_width
|
19
|
+
mattr_accessor :resolution
|
20
|
+
mattr_accessor :lazy_loading
|
21
|
+
mattr_accessor :lazy_loading_placeholder
|
22
|
+
mattr_accessor :require_alt_attribute
|
23
|
+
|
24
|
+
mattr_accessor :replace_active_storage_analyzer
|
25
|
+
mattr_accessor :check_for_white_background
|
26
|
+
|
27
|
+
mattr_accessor :convert_to_jpeg
|
28
|
+
mattr_accessor :jpeg_conversion
|
29
|
+
mattr_accessor :jpeg_optimization
|
30
|
+
mattr_accessor :png_optimization
|
31
|
+
mattr_accessor :active_storage_route
|
32
|
+
|
33
|
+
mattr_accessor :skip_ssl_verification
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
# This is an abstract base class for dimension calculators
|
5
|
+
class Analyzer
|
6
|
+
attr_reader :source
|
7
|
+
|
8
|
+
# Implement this method in a concrete subclass. Have it return true when given a source from which
|
9
|
+
# it can calculate dimensions
|
10
|
+
def self.accept?(source)
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(source)
|
15
|
+
@source = source
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger
|
19
|
+
VitalsImage.logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open-uri"
|
4
|
+
|
5
|
+
module VitalsImage
|
6
|
+
class Analyzer::Url < Analyzer
|
7
|
+
def self.accept?(source)
|
8
|
+
source.is_a?(Source)
|
9
|
+
end
|
10
|
+
|
11
|
+
def analyze
|
12
|
+
file = download
|
13
|
+
image = open(file)
|
14
|
+
mime = Marcel::MimeType.for(Pathname.new file.path)
|
15
|
+
|
16
|
+
source.update width: image.width, height: image.height, analyzed: true, content_type: mime
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def open(file)
|
21
|
+
if VitalsImage.image_library == :mini_magick
|
22
|
+
MiniMagick::Image.new(file.path).tap do |image|
|
23
|
+
raise "Invalid image" unless image.valid?
|
24
|
+
end
|
25
|
+
else
|
26
|
+
Vips::Image.new_from_file(file.path, access: :sequential).tap do |image|
|
27
|
+
image.avg
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def download
|
33
|
+
uri = URI.parse(source.key)
|
34
|
+
io = uri.open(ssl_verify_mode: ssl_verify_mode)
|
35
|
+
downloaded = Tempfile.new([File.basename(uri.path), File.extname(uri.path)])
|
36
|
+
|
37
|
+
if io.is_a?(Tempfile)
|
38
|
+
FileUtils.mv io.path, downloaded.path
|
39
|
+
else
|
40
|
+
# StringIO
|
41
|
+
File.write(downloaded.path, io.string)
|
42
|
+
end
|
43
|
+
|
44
|
+
downloaded
|
45
|
+
rescue
|
46
|
+
logger.error "Failed to download #{source.key}"
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssl_verify_mode
|
51
|
+
VitalsImage.skip_ssl_verification ? OpenSSL::SSL::VERIFY_NONE : nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Base
|
5
|
+
TINY_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
6
|
+
|
7
|
+
def self.optimizer(object, options = {})
|
8
|
+
klass = VitalsImage.optimizers.detect { |optimizer| optimizer.accept?(object) }
|
9
|
+
raise UnoptimizableError, "Object is not supported: #{object.class}" unless klass
|
10
|
+
|
11
|
+
klass.new(object, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.analyzer(object)
|
15
|
+
klass = VitalsImage.analyzers.detect { |analyzer| analyzer.accept?(object) }
|
16
|
+
raise UnanalyzableError, "Object is not supported: #{object.class}" unless klass
|
17
|
+
|
18
|
+
klass.new(object)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Cache
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@store = ActiveSupport::Cache::MemoryStore.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def locate(key)
|
12
|
+
source = @store.read(key)
|
13
|
+
|
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
|
19
|
+
|
20
|
+
source
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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
|
@@ -0,0 +1,72 @@
|
|
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
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_job"
|
4
|
+
require "active_model"
|
5
|
+
require "active_record"
|
6
|
+
require "active_storage"
|
7
|
+
require "active_support"
|
8
|
+
|
9
|
+
require "marcel"
|
10
|
+
require "ruby-vips"
|
11
|
+
require "mini_magick"
|
12
|
+
|
13
|
+
require "vitals_image/analyzer"
|
14
|
+
require "vitals_image/analyzer/url"
|
15
|
+
require "vitals_image/base"
|
16
|
+
require "vitals_image/cache"
|
17
|
+
require "vitals_image/errors"
|
18
|
+
require "vitals_image/optimizer"
|
19
|
+
require "vitals_image/optimizer/blank"
|
20
|
+
require "vitals_image/optimizer/url"
|
21
|
+
require "vitals_image/optimizer/active_storage"
|
22
|
+
|
23
|
+
module VitalsImage
|
24
|
+
class Engine < ::Rails::Engine
|
25
|
+
isolate_namespace VitalsImage
|
26
|
+
|
27
|
+
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
|
+
|
31
|
+
config.eager_load_namespaces << VitalsImage
|
32
|
+
|
33
|
+
initializer "vitals_image.configs" do
|
34
|
+
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
|
39
|
+
|
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
|
46
|
+
|
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
|
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
|
55
|
+
|
56
|
+
VitalsImage.skip_ssl_verification = app.config.vitals_image.skip_ssl_verification || false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
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
|
+
|
64
|
+
config.after_initialize do |app|
|
65
|
+
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
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
initializer "vitals_image.action_controller" do
|
76
|
+
ActiveSupport.on_load :action_controller do
|
77
|
+
helper VitalsImage::TagHelper
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
# Generic base class for all VitalsImage exceptions.
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised when VitalsImage is given a source it cannot optimize
|
8
|
+
class UnoptimizableError < Error; end
|
9
|
+
|
10
|
+
# Raised when VitalsImage is given a source it cannot extract metadata from
|
11
|
+
class UnanalyzableError < Error; end
|
12
|
+
|
13
|
+
# Raised when VitalsImage is given a url that it cannot download a file from
|
14
|
+
class FileNotFoundError < Error; end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
# Returns the version of the currently loaded Active Storage as a <tt>Gem::Version</tt>.
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 0
|
11
|
+
MINOR = 1
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
# This is an abstract base class for optimizers
|
5
|
+
class Optimizer
|
6
|
+
# Implement this method in a concrete subclass. Have it return true when given a source from which
|
7
|
+
# it can calculate dimensions
|
8
|
+
def self.accept?(source)
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(source, options = {})
|
13
|
+
@source = source
|
14
|
+
@options = options.deep_stringify_keys
|
15
|
+
|
16
|
+
raise ArgumentError, "You must specify an alt for your image" if @options["alt"].blank? && VitalsImage.require_alt_attribute
|
17
|
+
end
|
18
|
+
|
19
|
+
def src
|
20
|
+
if non_native_lazy_load?
|
21
|
+
VitalsImage.lazy_loading_placeholder
|
22
|
+
else
|
23
|
+
source_url
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def html_options
|
28
|
+
@html_options ||= begin
|
29
|
+
html_options = @options.dup
|
30
|
+
html_options["width"] = width
|
31
|
+
html_options["height"] = height
|
32
|
+
html_options["style"] = style
|
33
|
+
html_options["class"] = "vitals-image #{html_options["class"]}".squish
|
34
|
+
|
35
|
+
if non_native_lazy_load?
|
36
|
+
html_options["class"] = "#{VitalsImage.lazy_loading} #{html_options["class"]}".squish
|
37
|
+
html_options["data"] ||= {}
|
38
|
+
html_options["data"]["src"] = source_url
|
39
|
+
elsif lazy_load?
|
40
|
+
html_options["loading"] = "lazy"
|
41
|
+
html_options["decoding"] = "async"
|
42
|
+
end
|
43
|
+
|
44
|
+
html_options.compact
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def lazy_load?
|
49
|
+
@options["lazy_load"] != false && VitalsImage.lazy_loading
|
50
|
+
end
|
51
|
+
|
52
|
+
def non_native_lazy_load?
|
53
|
+
lazy_load? && VitalsImage.lazy_loading != :native
|
54
|
+
end
|
55
|
+
|
56
|
+
def native_lazy_load?
|
57
|
+
lazy_load? && VitalsImage.lazy_loading == :native
|
58
|
+
end
|
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
|
+
private
|
66
|
+
def style
|
67
|
+
if analyzed? && !requested_height
|
68
|
+
"height:auto;"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def width
|
73
|
+
if !analyzed? || fixed_dimensions?
|
74
|
+
requested_width
|
75
|
+
else
|
76
|
+
(original_width * scale).round
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def height
|
81
|
+
if !analyzed? || fixed_dimensions?
|
82
|
+
requested_height
|
83
|
+
else
|
84
|
+
(original_height * scale).round
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def scale
|
90
|
+
[scale_x, scale_y].min
|
91
|
+
end
|
92
|
+
|
93
|
+
def scale_x
|
94
|
+
requested_width ? requested_width / original_width : 1.0
|
95
|
+
end
|
96
|
+
|
97
|
+
def scale_y
|
98
|
+
requested_height ? requested_height / original_height : 1.0
|
99
|
+
end
|
100
|
+
|
101
|
+
def fixed_dimensions?
|
102
|
+
requested_width && requested_height
|
103
|
+
end
|
104
|
+
|
105
|
+
def requested_width
|
106
|
+
@options["width"].to_f if @options["width"]
|
107
|
+
end
|
108
|
+
|
109
|
+
def requested_height
|
110
|
+
@options["height"].to_f if @options["height"]
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Override this method in a concrete subclass. Have it return true if width and height are available
|
115
|
+
def analyzed?
|
116
|
+
raise NotImplementedError
|
117
|
+
end
|
118
|
+
|
119
|
+
# Override this method in a concrete subclass. Have it return the width of the image
|
120
|
+
def original_width
|
121
|
+
raise NotImplementedError
|
122
|
+
end
|
123
|
+
|
124
|
+
# Override this method in a concrete subclass. Have it return the height of the image
|
125
|
+
def original_height
|
126
|
+
raise NotImplementedError
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# Override this method in a concrete subclass. Have it return the url of the image
|
131
|
+
def source_url
|
132
|
+
raise NotImplementedError
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Optimizer::ActiveStorage < Optimizer
|
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
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def source_url
|
15
|
+
if analyzed?
|
16
|
+
variant
|
17
|
+
else
|
18
|
+
@source
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def original_width
|
23
|
+
metadata[:width]
|
24
|
+
end
|
25
|
+
|
26
|
+
def original_height
|
27
|
+
metadata[:height]
|
28
|
+
end
|
29
|
+
|
30
|
+
def metadata
|
31
|
+
@source.metadata
|
32
|
+
end
|
33
|
+
|
34
|
+
def analyzed?
|
35
|
+
metadata[:analyzed]
|
36
|
+
end
|
37
|
+
|
38
|
+
def alpha?
|
39
|
+
@options["alpha"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def dimensions
|
43
|
+
if fixed_dimensions?
|
44
|
+
[(requested_width * VitalsImage.resolution).floor, (requested_height * VitalsImage.resolution).floor]
|
45
|
+
elsif VitalsImage.resolution * scale > 1
|
46
|
+
[original_width, original_height]
|
47
|
+
else
|
48
|
+
[(width * VitalsImage.resolution).floor, (height * VitalsImage.resolution).floor]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def resize_mode
|
53
|
+
@options[:resize_mode] || @source.metadata["isolated"] ? :resize_and_pad : :resize_to_fill
|
54
|
+
end
|
55
|
+
|
56
|
+
def variant
|
57
|
+
case (@source.content_type)
|
58
|
+
when /jpg|jpeg/
|
59
|
+
optimize_jpeg
|
60
|
+
when /png/
|
61
|
+
optimize_png
|
62
|
+
else
|
63
|
+
optimize_generic
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def optimize_jpeg
|
68
|
+
@source.variant VitalsImage.jpeg_optimization.merge("#{resize_mode}": dimensions)
|
69
|
+
end
|
70
|
+
|
71
|
+
def optimize_png
|
72
|
+
if alpha? || !VitalsImage.convert_to_jpeg
|
73
|
+
@source.variant VitalsImage.png_optimization.merge("#{resize_mode}": dimensions)
|
74
|
+
else
|
75
|
+
@source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def optimize_generic
|
80
|
+
if alpha? || !VitalsImage.convert_to_jpeg
|
81
|
+
@source.variant("#{resize_mode}": dimensions)
|
82
|
+
else
|
83
|
+
@source.variant VitalsImage.jpeg_conversion.merge("#{resize_mode}": dimensions)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Optimizer::Blank < Optimizer
|
5
|
+
def self.accept?(source)
|
6
|
+
source.blank?
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def source_url
|
11
|
+
VitalsImage.lazy_loading_placeholder
|
12
|
+
end
|
13
|
+
|
14
|
+
def width
|
15
|
+
@options["width"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def height
|
19
|
+
@options["height"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def style
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def lazy_load?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VitalsImage
|
4
|
+
class Optimizer::Url < Optimizer
|
5
|
+
def self.accept?(source)
|
6
|
+
uri = URI.parse(source)
|
7
|
+
%w( http https ).include?(uri.scheme)
|
8
|
+
rescue URI::BadURIError
|
9
|
+
false
|
10
|
+
rescue URI::InvalidURIError
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def source_url
|
16
|
+
@source
|
17
|
+
end
|
18
|
+
|
19
|
+
def style
|
20
|
+
if !analyzed?
|
21
|
+
# Do nothing
|
22
|
+
elsif !requested_height
|
23
|
+
"height:auto;"
|
24
|
+
elsif fixed_dimensions?
|
25
|
+
"object-fit: contain;"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def analyzed?
|
30
|
+
metadata.analyzed
|
31
|
+
end
|
32
|
+
|
33
|
+
def original_width
|
34
|
+
metadata.width
|
35
|
+
end
|
36
|
+
|
37
|
+
def original_height
|
38
|
+
metadata.height
|
39
|
+
end
|
40
|
+
|
41
|
+
def metadata
|
42
|
+
Cache.instance.locate(@source)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vitals_image
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Breno Gazzola
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activemodel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '6.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '6.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activestorage
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '6.1'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '6.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: mini_magick
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.1'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: ruby-vips
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: platform_agent
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sqlite3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.4'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.4'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: byebug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '11.1'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '11.1'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.14'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.14'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop-performance
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '1.11'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '1.11'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: rubocop-packaging
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0.5'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0.5'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rubocop-rails
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '2.10'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '2.10'
|
209
|
+
description:
|
210
|
+
email:
|
211
|
+
- breno@festalab.com
|
212
|
+
executables: []
|
213
|
+
extensions: []
|
214
|
+
extra_rdoc_files: []
|
215
|
+
files:
|
216
|
+
- MIT-LICENSE
|
217
|
+
- Rakefile
|
218
|
+
- app/assets/config/vitals_image_manifest.js
|
219
|
+
- app/assets/stylesheets/vitals_image/application.css
|
220
|
+
- app/controllers/vitals_image/application_controller.rb
|
221
|
+
- app/helpers/vitals_image/application_helper.rb
|
222
|
+
- app/helpers/vitals_image/tag_helper.rb
|
223
|
+
- app/jobs/vitals_image/analyze_job.rb
|
224
|
+
- app/jobs/vitals_image/application_job.rb
|
225
|
+
- app/models/vitals_image/application_record.rb
|
226
|
+
- app/models/vitals_image/source.rb
|
227
|
+
- app/views/layouts/vitals_image/application.html.erb
|
228
|
+
- config/routes.rb
|
229
|
+
- db/migrate/20210502132155_create_vitals_image_sources.rb
|
230
|
+
- lib/tasks/vitals_image_tasks.rake
|
231
|
+
- lib/vitals_image.rb
|
232
|
+
- lib/vitals_image/analyzer.rb
|
233
|
+
- lib/vitals_image/analyzer/url.rb
|
234
|
+
- lib/vitals_image/base.rb
|
235
|
+
- 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
|
+
- lib/vitals_image/engine.rb
|
239
|
+
- lib/vitals_image/errors.rb
|
240
|
+
- lib/vitals_image/gem_version.rb
|
241
|
+
- lib/vitals_image/optimizer.rb
|
242
|
+
- lib/vitals_image/optimizer/active_storage.rb
|
243
|
+
- lib/vitals_image/optimizer/blank.rb
|
244
|
+
- lib/vitals_image/optimizer/url.rb
|
245
|
+
- lib/vitals_image/test_case.rb
|
246
|
+
- lib/vitals_image/version.rb
|
247
|
+
homepage: https://github.com/FestaLab/vitals_image
|
248
|
+
licenses:
|
249
|
+
- MIT
|
250
|
+
metadata:
|
251
|
+
homepage_uri: https://github.com/FestaLab/vitals_image
|
252
|
+
source_code_uri: https://github.com/FestaLab/vitals_image
|
253
|
+
changelog_uri: https://github.com/FestaLab/vitals_image/CHANGELOG.mg
|
254
|
+
post_install_message:
|
255
|
+
rdoc_options: []
|
256
|
+
require_paths:
|
257
|
+
- lib
|
258
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
259
|
+
requirements:
|
260
|
+
- - ">="
|
261
|
+
- !ruby/object:Gem::Version
|
262
|
+
version: 2.5.0
|
263
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
264
|
+
requirements:
|
265
|
+
- - ">="
|
266
|
+
- !ruby/object:Gem::Version
|
267
|
+
version: '0'
|
268
|
+
requirements: []
|
269
|
+
rubygems_version: 3.2.17
|
270
|
+
signing_key:
|
271
|
+
specification_version: 4
|
272
|
+
summary: Image tags that conform with web vitals
|
273
|
+
test_files: []
|