derived_images 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6de1adc7c9f26d102288dc2d6eccc8581fbe3b0f8ae33934d5ffb3c0b4842b6d
4
+ data.tar.gz: 5f45a38c7566beaf40dc61861e4b29567add7ff87bcc12d14a5cb7bb53a6ee1f
5
+ SHA512:
6
+ metadata.gz: 91401997195e143d4daa30e136d8b55e206f3b98f209488bc4a8545f457a6bf9391363d64d281678af5df2bd4c1a0ad047953e1c9fb5c7526eecdf276fb60f53
7
+ data.tar.gz: 5f24b1d1028eafcd016277b0c98ff26abd660c708675374e4360fb640ca12802efdf3412a77710ea0aea2c0a0c54836b4070cd2550f9a272a0df441f91db7a83
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Michael Kitson
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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # DerivedImages
2
+
3
+ ActiveStorage's `variant` functionality is awesome, so why not have it for images in the Rails asset pipeline too?
4
+
5
+ Use DerivedImages to programmatically create image assets by applying transformations to other assets.
6
+ Resize images, lower quality to save bytes, rotate, crop, convert between formats, and anything else that the
7
+ [image_processing](https://rubygems.org/gems/image_processing) gem supports.
8
+
9
+ ## Installation
10
+
11
+ 1. Run `./bin/bundle add derived_images`
12
+ 2. Run `./bin/rails derived_images:install`
13
+ 3. Install Vips or ImageMagick (if not already installed for ActiveStorage/ImageProcessing)
14
+ - MacOS: `brew install vips` or `brew install imagemagick`
15
+ - Debian/Ubuntu: `apt install libvips42` or `apt install imagemagick`
16
+
17
+ ## Usage
18
+
19
+ After installing, specify images to create in the config file at `config/derived_images.rb`.
20
+
21
+ ```ruby
22
+ derive 'my_derived_image.webp', from: 'my_source_image.jpg'
23
+ resize 'tiny.png', from: 'original.png', width: 400, height: 300
24
+ derive 'fully_custom.jpg', from: 'original.jpg' do |pipeline|
25
+ pipeline.saver(quality: 50).resize_to_fill(80, 80).rotate(180)
26
+ end
27
+ ```
28
+
29
+ DerivedImages watches this file and the source images, compiling new assets into `app/assets/builds` where the asset
30
+ pipeline can pick them up with the normal asset helpers.
31
+
32
+ ```erbruby
33
+ <%= image_tag('tiny.png') %>
34
+ ```
35
+
36
+ ## License
37
+
38
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
5
+ require 'rake/testtask'
6
+
7
+ task default: :test
8
+ Rake::TestTask.new do |t|
9
+ t.libs << 'test'
10
+ t.pattern = 'test/*_test.rb'
11
+ t.warning = false
12
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # Build cache for derived image files.
5
+ class Cache
6
+ include Enumerable
7
+
8
+ # @param [Pathname, String, nil] path The cache directory, which will be created if missing. Nil disables caching.
9
+ def initialize(path = DerivedImages.config.cache_path)
10
+ @path = path && Pathname.new(path)
11
+ end
12
+
13
+ # Copy a cached file out to another path.
14
+ #
15
+ # @param [String] key Cache key
16
+ # @param [Pathname, String] path
17
+ def copy(key, path)
18
+ enabled? && FileUtils.copy(key_path(key), path)
19
+ end
20
+
21
+ # Check for the presence of a cached file.
22
+ #
23
+ # @param [String] key Cache key
24
+ # @return [Boolean]
25
+ def exist?(key)
26
+ enabled? && key_path(key).file?
27
+ end
28
+
29
+ # Remove a cached file, if it exists.
30
+ #
31
+ # @param [String] key Cache key
32
+ def remove(key)
33
+ return unless enabled?
34
+
35
+ key_path(key).delete if key_path(key).exist?
36
+ maybe_clean_dir_for(key)
37
+ end
38
+
39
+ # Copy a file into the cache.
40
+ #
41
+ # @param [String] key Cache key
42
+ # @param [Pathname, String] path
43
+ def store(key, path)
44
+ return unless enabled?
45
+
46
+ mkdir_for(key)
47
+ FileUtils.copy(path, key_path(key))
48
+ end
49
+
50
+ # Move (not copy) a file into the cache.
51
+ #
52
+ # @param [String] key Cache key
53
+ # @param [Pathname, String] path
54
+ def take_and_store(key, path)
55
+ return unless enabled?
56
+
57
+ mkdir_for(key)
58
+ FileUtils.mv(path, key_path(key))
59
+ end
60
+
61
+ # Convert a cache key into a filesystem path of where it would be stored.
62
+ #
63
+ # @param [String] key Cache key
64
+ # @return [Pathname]
65
+ def key_path(key)
66
+ path.join(key[0...2], key[2..])
67
+ end
68
+
69
+ # Iterates over cache keys.
70
+ #
71
+ # @yieldparam key [String] the cache key
72
+ # @return [DerivedImages::Cache, Enumerator]
73
+ def each
74
+ return enum_for(:each) unless block_given?
75
+
76
+ path.glob('*/*') { |path| yield path.to_s.last(65).delete('/') }
77
+ self
78
+ end
79
+
80
+ # Get the SHA256 hash of a cached file.
81
+ #
82
+ # @param [String] key Cache key
83
+ # @return [String, nil] The hex-encoded digest
84
+ def digest(key)
85
+ Digest::SHA256.file(key_path(key)).hexdigest if exist?(key)
86
+ end
87
+
88
+ private
89
+
90
+ attr_reader :path
91
+
92
+ # Create parent directories so that we can store a value at the given key.
93
+ #
94
+ # @param [String] key Cache key
95
+ def mkdir_for(key)
96
+ FileUtils.mkdir_p(key_path(key).dirname)
97
+ end
98
+
99
+ # Clean up any unnecessary parent directories above the given cache key.
100
+ #
101
+ # @param [String] key Cache key
102
+ def maybe_clean_dir_for(key)
103
+ dir = key_path(key).dirname
104
+ dir.rmdir if dir.directory? && dir.empty?
105
+ end
106
+
107
+ def enabled?
108
+ !@path.nil?
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # Use these DSL functions in your `config/derived_images.rb` file to define images to derive.
5
+ #
6
+ # @note An image format conversions can be automatically inferred from the `target` file extension in these methods.
7
+ module Dsl
8
+ # Resize an image, preserving its aspect ratio.
9
+ #
10
+ # @param [String] target The relative file name of the target (derived) image
11
+ # @param [String] from The relative file name of the source image. Must be in one of the configured `image_paths`
12
+ # @param [Integer] width The max width of the derived image
13
+ # @param [Integer] height The max height of the derived image
14
+ # @return [ManifestEntry]
15
+ def resize(target, from:, width:, height:)
16
+ derive(target, from: from) { _1.resize_to_limit(width, height) }
17
+ end
18
+
19
+ # Derive one image from another, with full customization abilities.
20
+ #
21
+ # @param [String] target The relative file name of the target (derived) image
22
+ # @param [String] from The relative file name of the source image. Must be in one of the configured `image_paths`
23
+ # @yieldparam pipeline [ImageProcessing::Chainable] The pipeline you can use to further customize the transformation
24
+ # @return [ManifestEntry]
25
+ def derive(target, from:, &block)
26
+ pipeline = ManifestEntry.empty_pipeline
27
+ pipeline = yield(pipeline) if block
28
+ add_entry(ManifestEntry.new(from, target, pipeline))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # The Manifest creates and holds a list of {ManifestEntry} instances, describing every derived image to create.
5
+ class Manifest
6
+ include Dsl
7
+
8
+ def initialize(path = Rails.root.join(DerivedImages.config.manifest_path))
9
+ @path = path
10
+ @entry_map = {}
11
+ end
12
+
13
+ def draw(&block)
14
+ @entry_map.clear
15
+ if block
16
+ instance_eval(&block)
17
+ else
18
+ instance_eval(File.read(path), path.to_s)
19
+ end
20
+ end
21
+
22
+ def add_entry(entry)
23
+ entry_map[entry.target] = entry
24
+ end
25
+
26
+ delegate :[], :count, :each, :each_value, :filter_map, :key?, :length, to: :entry_map
27
+
28
+ def produced_from(source_path)
29
+ source_names = []
30
+ DerivedImages.config.image_paths.each do |path|
31
+ dir = Pathname.new(path).expand_path.realpath
32
+ contains_source_file = source_path.ascend.any? { _1 == dir }
33
+ source_names << source_path.relative_path_from(dir).to_s if contains_source_file
34
+ end
35
+ entry_map.filter_map { |_target, entry| source_names.include?(entry.source) ? entry : nil }
36
+ end
37
+
38
+ attr_reader :path
39
+
40
+ def diff_from(former_manifest)
41
+ changed = []
42
+ removed = []
43
+ former_manifest.each do |target, entry|
44
+ if key?(target)
45
+ changed << entry if entry_map[target] != entry
46
+ else
47
+ removed << entry
48
+ end
49
+ end
50
+ added = entry_map.filter_map { |target, entry| former_manifest.key?(target) ? nil : entry }
51
+ [added, changed, removed]
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :entry_map
57
+ end
58
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # A ManifestEntry describes how to create one derived image.
5
+ class ManifestEntry
6
+ attr_accessor :source, :target, :pipeline
7
+
8
+ def initialize(source, target, pipeline)
9
+ @source = source
10
+ @target = target
11
+ @pipeline = pipeline
12
+ end
13
+
14
+ def ==(other)
15
+ source == other.source && target == other.target && options_hash == other.options_hash
16
+ end
17
+
18
+ def source_path
19
+ DerivedImages.config.image_paths.each do |path|
20
+ path = Pathname.new(path).join(source).expand_path
21
+ return path if path.file?
22
+ end
23
+ nil
24
+ end
25
+
26
+ def target_path
27
+ Pathname.new(DerivedImages.config.build_path).join(target).expand_path
28
+ end
29
+
30
+ # Returns a cache key for the result of the image transformation. It will vary if the source file's content varies
31
+ # or if the operations applied to generate the target vary.
32
+ #
33
+ # @return [String, nil] A 64 character hexdigest, or nil if the source file can't be found
34
+ def cache_key
35
+ return nil unless source_present?
36
+
37
+ Digest::SHA256.hexdigest({ source: source_digest, pipeline: options_hash }.to_json)
38
+ end
39
+
40
+ def self.empty_pipeline
41
+ PROCESSORS.fetch(DerivedImages.config.processor).dup
42
+ end
43
+
44
+ PROCESSORS = { mini_magick: ImageProcessing::MiniMagick, vips: ImageProcessing::Vips }.freeze
45
+
46
+ def target_digest
47
+ Digest::SHA256.file(target_path).hexdigest
48
+ end
49
+
50
+ def source_present?
51
+ source_path&.file?
52
+ end
53
+
54
+ private
55
+
56
+ def source_digest
57
+ Digest::SHA256.file(source_path).hexdigest
58
+ end
59
+
60
+ protected
61
+
62
+ def options_hash
63
+ pipeline.branch.options
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # The Processor manages a {Manifest}, watches the filesystem for changes, and manages a pool of {Worker} instances
5
+ # which perform the image tasks.
6
+ class Processor
7
+ def initialize
8
+ @cache = Cache.new
9
+ @manifest = Manifest.new.tap(&:draw)
10
+ @queue = Thread::Queue.new
11
+ @workers = ThreadGroup.new
12
+ end
13
+
14
+ def watch
15
+ watch_manifest
16
+ watch_images
17
+ process_all
18
+ end
19
+
20
+ def unwatch
21
+ manifest_listener&.stop
22
+ image_listener&.stop
23
+ end
24
+
25
+ def run_once
26
+ process_all
27
+ queue.close
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :cache, :image_listener, :manifest, :manifest_listener, :queue, :workers
33
+
34
+ def watch_manifest
35
+ dir, file = manifest.path.split.map(&:to_s)
36
+ @manifest_listener = Listen.to(dir, only: Regexp.new(file)) do
37
+ DerivedImages.config.logger.debug('Reloading changed manifest')
38
+ @manifest = Manifest.new.tap(&:draw)
39
+ process_all
40
+ end
41
+ manifest_listener.start
42
+ end
43
+
44
+ def watch_images
45
+ @image_listener = Listen.to(*DerivedImages.config.image_paths) do |modified, added, removed|
46
+ (modified + added + removed).each do |path|
47
+ source_path = Pathname.new(path).expand_path.realpath
48
+ manifest.produced_from(source_path).each { enqueue(_1) }
49
+ end
50
+ prune_cache
51
+ end
52
+ image_listener.start
53
+ end
54
+
55
+ def process_all
56
+ manifest.each_value { enqueue(_1) }
57
+ prune_cache
58
+ end
59
+
60
+ def enqueue(entry)
61
+ should_expand = queue.num_waiting.zero? && workers.list.length < DerivedImages.config.threads
62
+ queue << entry
63
+ Worker.start(workers, queue) if should_expand
64
+ end
65
+
66
+ def prune_cache
67
+ expected_keys = manifest.filter_map { |_target, entry| entry.cache_key }
68
+ (cache.to_a - expected_keys).each do |key|
69
+ DerivedImages.config.logger.debug("Removing cached file at #{key}")
70
+ cache.remove(key)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # The Railtie runs the {Processor} as part of a rails server.
5
+ class Railtie < ::Rails::Railtie
6
+ config.derived_images = ActiveSupport::OrderedOptions.new.update(
7
+ build_path: 'app/assets/builds',
8
+ cache_path: Rails.env.development? ? 'tmp/cache/derived_images' : nil,
9
+ enabled?: Rails.env.development? || Rails.env.test?,
10
+ image_paths: ['app/assets/images'],
11
+ manifest_path: 'config/derived_images.rb',
12
+ processor: :vips,
13
+ threads: 5,
14
+ watch?: Rails.env.development?
15
+ )
16
+
17
+ initializer 'derived_images' do |app|
18
+ derived_images = app.config.derived_images
19
+ derived_images.logger = Rails.logger.tagged('derived_images')
20
+ end
21
+
22
+ rake_tasks do
23
+ load 'tasks/derived_images.rake'
24
+ end
25
+
26
+ server do |app|
27
+ derived_images = app.config.derived_images
28
+ next unless derived_images.enabled?
29
+
30
+ processor = DerivedImages::Processor.new
31
+ if derived_images.watch?
32
+ processor.watch
33
+ else
34
+ processor.run_once
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ VERSION = '0.3.0'
5
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DerivedImages
4
+ # A Worker represents a thread in a thread pool that completes image creation tasks.
5
+ class Worker
6
+ def initialize(queue, cache = Cache.new)
7
+ @queue = queue
8
+ @cache = cache
9
+ end
10
+
11
+ def run
12
+ until queue.closed? && queue.empty?
13
+ entry = queue.pop
14
+ process(entry) if entry
15
+ end
16
+ end
17
+
18
+ def self.start(thread_group, queue)
19
+ DerivedImages.config.logger.debug('Starting a new worker thread')
20
+ thread_group.add(Thread.new { Worker.new(queue).run })
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :cache, :queue
26
+
27
+ def process(entry)
28
+ unless entry.source_present?
29
+ return DerivedImages.config.logger.error("Can't find #{entry.source} to build #{entry.target}")
30
+ end
31
+
32
+ cache_key = entry.cache_key
33
+ if cache.exist?(cache_key)
34
+ restore(entry, cache_key)
35
+ else
36
+ generate(entry, cache_key)
37
+ end
38
+ end
39
+
40
+ def restore(entry, cache_key)
41
+ return if entry.target_path.file? && cache.digest(cache_key) == entry.target_digest
42
+
43
+ cache.copy(cache_key, entry.target_path)
44
+ DerivedImages.config.logger.debug("Restored #{entry.target} from cache")
45
+ end
46
+
47
+ def generate(entry, cache_key)
48
+ time = Benchmark.realtime do
49
+ tempfile = entry.pipeline.loader(fail: true).call(entry.source_path.to_s)
50
+ FileUtils.mv(tempfile.path, entry.target_path)
51
+ end
52
+ cache.store(cache_key, entry.target_path)
53
+ DerivedImages.config.logger.info("Created #{entry.target} from #{entry.source} in #{time.round(3)}s")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_processing'
4
+ require 'listen'
5
+
6
+ require 'derived_images/cache'
7
+ require 'derived_images/dsl'
8
+ require 'derived_images/manifest'
9
+ require 'derived_images/manifest_entry'
10
+ require 'derived_images/processor'
11
+ require 'derived_images/railtie'
12
+ require 'derived_images/worker'
13
+ require 'derived_images/version'
14
+
15
+ # DerivedImages programmatically creates derived image assets by applying transformations to other assets.
16
+ module DerivedImages
17
+ def self.config
18
+ Rails.application.config.derived_images
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Define your derived images using the DSL in lib/derived_images/dsl.rb
4
+ # derive "hero_1200.webp", from: "hero_1200.png"
5
+ # resize "favicon-32x32.png", from: "favicon.svg", width: 32, height: 32
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ say 'Create derived_images.rb manifest'
4
+ copy_file "#{__dir__}/derived_images.rb", 'config/derived_images.rb'
5
+
6
+ say 'Compile into app/assets/builds'
7
+ empty_directory 'app/assets/builds'
8
+ keep_file 'app/assets/builds'
9
+
10
+ if (sprockets_manifest_path = Rails.root.join('app/assets/config/manifest.js')).exist?
11
+ append_to_file sprockets_manifest_path, %(//= link_tree ../builds\n)
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :derived_images do
4
+ desc 'install derived_images'
5
+ task :install do
6
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path('../install/install.rb',
7
+ __dir__)}"
8
+ end
9
+
10
+ desc 'build assets from derived_images once'
11
+ task :build do
12
+ DerivedImages::Processor.new.run_once
13
+ end
14
+ end
15
+
16
+ Rake::Task['assets:precompile'].enhance(['derived_images:build']) if Rake::Task.task_defined?('assets:precompile')
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: derived_images
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Kitson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: image_processing
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: listen
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: railties
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
+ description: |-
56
+ Resize images, lower quality to save bytes, rotate, crop, convert between formats, and anything \
57
+ else that the image_processing gem supports.
58
+ email:
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/derived_images.rb
67
+ - lib/derived_images/cache.rb
68
+ - lib/derived_images/dsl.rb
69
+ - lib/derived_images/manifest.rb
70
+ - lib/derived_images/manifest_entry.rb
71
+ - lib/derived_images/processor.rb
72
+ - lib/derived_images/railtie.rb
73
+ - lib/derived_images/version.rb
74
+ - lib/derived_images/worker.rb
75
+ - lib/install/derived_images.rb
76
+ - lib/install/install.rb
77
+ - lib/tasks/derived_images.rake
78
+ homepage: https://github.com/michaelkitson/derived_images
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ homepage_uri: https://github.com/michaelkitson/derived_images
83
+ source_code_uri: https://github.com/michaelkitson/derived_images
84
+ changelog_uri: https://github.com/michaelkitson/derived_images/releases
85
+ rubygems_mfa_required: 'true'
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 2.7.0
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.4.13
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Programmatically create derived image assets by applying transformations
105
+ to other assets.
106
+ test_files: []