derived_images 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []