dynamic_image 3.0.9 → 3.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 +4 -4
- data/README.md +393 -67
- data/app/models/dynamic_image/variant.rb +10 -0
- data/lib/dynamic_image/backfill.rb +85 -0
- data/lib/dynamic_image/belongs_to.rb +22 -0
- data/lib/dynamic_image/breakpoints.rb +93 -0
- data/lib/dynamic_image/controller.rb +51 -7
- data/lib/dynamic_image/digest_verifier.rb +20 -9
- data/lib/dynamic_image/engine.rb +3 -0
- data/lib/dynamic_image/errors.rb +30 -0
- data/lib/dynamic_image/format.rb +149 -8
- data/lib/dynamic_image/format_negotiator.rb +84 -0
- data/lib/dynamic_image/helper/formats.rb +57 -0
- data/lib/dynamic_image/helper/pictures.rb +124 -0
- data/lib/dynamic_image/helper.rb +100 -60
- data/lib/dynamic_image/image_processor/colors.rb +3 -7
- data/lib/dynamic_image/image_processor/frames.rb +10 -1
- data/lib/dynamic_image/image_processor/transform.rb +17 -2
- data/lib/dynamic_image/image_processor.rb +33 -6
- data/lib/dynamic_image/image_reader.rb +19 -1
- data/lib/dynamic_image/image_sizing.rb +51 -19
- data/lib/dynamic_image/metadata.rb +56 -5
- data/lib/dynamic_image/model/dimensions.rb +32 -13
- data/lib/dynamic_image/model/transformations.rb +47 -11
- data/lib/dynamic_image/model/validations.rb +15 -8
- data/lib/dynamic_image/model/variants.rb +6 -3
- data/lib/dynamic_image/model.rb +58 -34
- data/lib/dynamic_image/picture/format_policy.rb +72 -0
- data/lib/dynamic_image/picture.rb +215 -0
- data/lib/dynamic_image/processed_image.rb +48 -14
- data/lib/dynamic_image/ratio.rb +43 -0
- data/lib/dynamic_image/routing.rb +14 -3
- data/lib/dynamic_image/schema.rb +47 -0
- data/lib/dynamic_image/version.rb +1 -1
- data/lib/dynamic_image.rb +61 -1
- data/lib/rails/generators/dynamic_image/resource/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/resource/resource_generator.rb +57 -21
- data/lib/rails/generators/dynamic_image/resource/templates/create_table_migration.rb.tt +32 -0
- data/lib/rails/generators/dynamic_image/upgrade/USAGE +17 -0
- data/lib/rails/generators/dynamic_image/upgrade/templates/upgrade_migration.rb.tt +10 -0
- data/lib/rails/generators/dynamic_image/upgrade/upgrade_generator.rb +111 -0
- data/lib/tasks/dynamic_image.rake +25 -0
- metadata +23 -12
- data/lib/dynamic_image/jobs/create_variant.rb +0 -22
- data/lib/dynamic_image/jobs.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7d6b0fd60017fe14ffc6a01f99519802232b8bba0f94b756c9026cdf57c6897
|
|
4
|
+
data.tar.gz: 2c0c56a8b4505684e883fd5b74985b69ac9e6f190a41477087fa4f62827eedb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75c4599b900f98fa6c51adc140650b400ba19f7749e89e06d06b8ce580e61db32a3b7e3890cc35023c80b7b77da89400c1a74fa41e926e5ea0bd7cc516148ef6
|
|
7
|
+
data.tar.gz: 7a8573f7eb274a4f24842490ef296f50f750ba187f5a49b896b42fa0d4722fa255ed811151517c5d024094512e08c716855a4780050b22c04ef26ebbf6546174
|
data/README.md
CHANGED
|
@@ -1,38 +1,26 @@
|
|
|
1
1
|
[](https://rubygems.org/gems/dynamic_image)
|
|
2
|
-
](https://github.com/elektronaut/dynamic_image/actions/workflows/build.yml)
|
|
3
3
|
|
|
4
4
|
# DynamicImage
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
Give DynamicImage a try.
|
|
6
|
+
A Rails engine for image uploads.
|
|
8
7
|
|
|
9
8
|
Rather than creating a pre-defined set of images when a file is
|
|
10
9
|
uploaded, DynamicImage stores the original file and generates images
|
|
11
10
|
on demand. It handles cropping, resizing, format and colorspace
|
|
12
11
|
conversion.
|
|
13
12
|
|
|
14
|
-
Supported formats at the moment are JPEG, PNG, GIF, BMP, WebP and TIFF.
|
|
15
|
-
BMP, WebP and TIFF images will automatically be converted to JPG. CMYK
|
|
16
|
-
images will be converted to RGB, and RGB images will be converted to the sRGB
|
|
17
|
-
colorspace for consistent appearance in all browsers.
|
|
18
|
-
|
|
19
13
|
DynamicImage is built on [Dis](https://github.com/elektronaut/dis)
|
|
20
14
|
and [ruby-vips](https://github.com/libvips/ruby-vips).
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
and enumeration attacks.
|
|
24
|
-
|
|
25
|
-
## Requirements
|
|
26
|
-
|
|
27
|
-
* Rails 5+
|
|
28
|
-
* Ruby 2.7+
|
|
29
|
-
* libvips 8.8+
|
|
30
|
-
|
|
31
|
-
## Documentation
|
|
16
|
+
## Installation
|
|
32
17
|
|
|
33
|
-
|
|
18
|
+
DynamicImage requires [libvips](https://www.libvips.org), which is
|
|
19
|
+
available from most package managers.
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
```sh
|
|
22
|
+
brew install vips
|
|
23
|
+
```
|
|
36
24
|
|
|
37
25
|
Add the gem to your Gemfile and run `bundle install`.
|
|
38
26
|
|
|
@@ -40,102 +28,440 @@ Add the gem to your Gemfile and run `bundle install`.
|
|
|
40
28
|
gem "dynamic_image", "~> 3.0"
|
|
41
29
|
```
|
|
42
30
|
|
|
43
|
-
Run the `dis:install` generator to set up your storage.
|
|
31
|
+
Run the `dis:install` generator to set up your storage. Files are stored
|
|
32
|
+
in `db/dis` by default; edit the generated initializer to change that.
|
|
33
|
+
See the [Dis](https://github.com/elektronaut/dis) documentation for the
|
|
34
|
+
options.
|
|
44
35
|
|
|
45
36
|
```sh
|
|
46
37
|
bin/rails generate dis:install
|
|
47
38
|
```
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
will store files in `db/dis`. See the
|
|
51
|
-
[Dis](https://github.com/elektronaut/dis) documentation for more
|
|
52
|
-
information.
|
|
40
|
+
## Getting started
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
### Creating your resource
|
|
55
43
|
|
|
56
|
-
|
|
44
|
+
The `dynamic_image:resource` generator creates an `Image` model and a
|
|
45
|
+
controller, along with a migration and the necessary routes. The
|
|
46
|
+
migration creates the table for your resource, plus the table
|
|
47
|
+
DynamicImage uses to cache processed images.
|
|
57
48
|
|
|
58
49
|
```sh
|
|
59
50
|
bin/rails generate dynamic_image:resource image
|
|
51
|
+
bin/rails db:migrate
|
|
60
52
|
```
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
the
|
|
64
|
-
|
|
65
|
-
Note that in this case, the route with collide with any static images stored
|
|
66
|
-
in `public/images`. You can customize the path if you want in the route
|
|
67
|
-
declaration.
|
|
54
|
+
The generated route collides with any static images stored in
|
|
55
|
+
`public/images`. Customize the path in the route declaration if that's a
|
|
56
|
+
problem.
|
|
68
57
|
|
|
69
58
|
```ruby
|
|
70
59
|
image_resources :images, path: "dynamic_images/:digest(/:size)"
|
|
71
60
|
```
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Setting the model up by hand</summary>
|
|
64
|
+
|
|
65
|
+
Include `DynamicImage::Model` and provide these columns. The first four
|
|
66
|
+
are required by Dis, the rest hold the image metadata and crop.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
create_table :images do |t|
|
|
70
|
+
t.string :content_hash, null: false
|
|
71
|
+
t.string :content_type, null: false
|
|
72
|
+
t.integer :content_length, null: false
|
|
73
|
+
t.string :filename, null: false
|
|
74
|
+
t.string :colorspace, null: false
|
|
75
|
+
t.integer :real_width, null: false
|
|
76
|
+
t.integer :real_height, null: false
|
|
77
|
+
t.integer :frame_count
|
|
78
|
+
t.boolean :alpha
|
|
79
|
+
t.integer :crop_width, :crop_height
|
|
80
|
+
t.integer :crop_start_x, :crop_start_y
|
|
81
|
+
t.integer :crop_gravity_x, :crop_gravity_y
|
|
82
|
+
t.timestamps
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
add_index :images, :content_hash
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The controller needs `DynamicImage::Controller` and a `model` method
|
|
89
|
+
telling it which class to serve.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class ImagesController < ApplicationController
|
|
93
|
+
include DynamicImage::Controller
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def model
|
|
98
|
+
Image
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
### Storing an image
|
|
74
106
|
|
|
75
|
-
To save an image,
|
|
107
|
+
To save an image, assign the uploaded file to the `file` attribute.
|
|
76
108
|
|
|
77
109
|
```ruby
|
|
78
|
-
|
|
79
|
-
Image.create(image_params)
|
|
110
|
+
Image.create(params.expect(image: [:file]))
|
|
80
111
|
```
|
|
81
112
|
|
|
82
|
-
|
|
113
|
+
The image is parsed and validated when the record is saved. Dimensions,
|
|
114
|
+
colorspace and content type are read from the file itself, so anything
|
|
115
|
+
the client claims about the upload is ignored. If the file isn't a
|
|
116
|
+
readable image in a supported format, the record is invalid and an error
|
|
117
|
+
is added to `data`.
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
that the generated URLs are properly signed and timestamped.
|
|
119
|
+
### Associating images with other models
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
`belongs_to_image` works like `belongs_to` and takes the same options.
|
|
122
|
+
In addition to a record, it accepts an uploaded file directly and
|
|
123
|
+
creates the image record for you, so a file can be posted straight to
|
|
124
|
+
the parent model.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
class User < ActiveRecord::Base
|
|
128
|
+
belongs_to_image :avatar, class_name: "Image"
|
|
129
|
+
validates_associated :avatar
|
|
130
|
+
end
|
|
131
|
+
```
|
|
89
132
|
|
|
90
133
|
```erb
|
|
91
|
-
<%=
|
|
134
|
+
<%= form_with(model: user) do |f| %>
|
|
135
|
+
<%= f.file_field :avatar %>
|
|
136
|
+
<% end %>
|
|
92
137
|
```
|
|
93
138
|
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
```ruby
|
|
140
|
+
User.create(params.expect(user: [:name, :avatar]))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`validates_associated` is optional, but without it an invalid upload is
|
|
144
|
+
silently dropped when the parent is saved. Like `belongs_to`, the
|
|
145
|
+
association is required by default; pass `optional: true` if the image
|
|
146
|
+
is allowed to be missing.
|
|
147
|
+
|
|
148
|
+
## Image URLs
|
|
149
|
+
|
|
150
|
+
`dynamic_image_path` and `dynamic_image_url` act pretty much like
|
|
151
|
+
regular URL helpers, and take the sizing options below.
|
|
96
152
|
|
|
97
153
|
```erb
|
|
98
|
-
<%=
|
|
154
|
+
<%= link_to "See image", dynamic_image_path(image, size: "400x400") %>
|
|
99
155
|
```
|
|
100
156
|
|
|
101
|
-
|
|
157
|
+
Every URL is signed and timestamped, so they can only be built
|
|
158
|
+
server-side. See [signed URLs](#signed-urls) for why.
|
|
159
|
+
|
|
160
|
+
### Sizing options
|
|
161
|
+
|
|
162
|
+
Every helper that renders or links to a processed image takes the same
|
|
163
|
+
sizing options.
|
|
164
|
+
|
|
165
|
+
* `:size` - Desired image size, as `"{width}x{height}"`. The image is
|
|
166
|
+
scaled to fit within the size, preserving the aspect ratio. Omit
|
|
167
|
+
either dimension (`"400x"` or `"x400"`) for a fixed width or height.
|
|
168
|
+
* `:crop` - Crop the image to the exact size instead of fitting it.
|
|
169
|
+
Both dimensions are required.
|
|
170
|
+
* `:upscale` - By default, images are never scaled up, only down. Pass
|
|
171
|
+
`upscale: true` to allow the image to be scaled beyond its own size.
|
|
172
|
+
* `:format` - Render the image in a different format. See
|
|
173
|
+
[Formats](#formats).
|
|
174
|
+
|
|
175
|
+
### Other versions of the image
|
|
176
|
+
|
|
177
|
+
Three variations are available. `uncropped` has `_tag`, `_path` and
|
|
178
|
+
`_url` helpers; `original` and `download` have `_path` and `_url`.
|
|
179
|
+
`uncropped` takes the sizing options; `original` and `download` serve
|
|
180
|
+
the stored file untouched and ignore them.
|
|
181
|
+
|
|
182
|
+
* `original_dynamic_image_path` links to the file exactly as it was
|
|
183
|
+
uploaded, with no processing at all.
|
|
184
|
+
* `download_dynamic_image_path` serves the original as an attachment,
|
|
185
|
+
prompting a download.
|
|
186
|
+
* `uncropped_dynamic_image_tag` renders the image with any pre-cropping
|
|
187
|
+
ignored.
|
|
188
|
+
|
|
189
|
+
```erb
|
|
190
|
+
<%= link_to "Download", download_dynamic_image_path(image) %>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Displaying images
|
|
194
|
+
|
|
195
|
+
`dynamic_image_tag` renders an `img`, taking the sizing options above
|
|
196
|
+
plus any HTML attributes.
|
|
102
197
|
|
|
103
198
|
```erb
|
|
199
|
+
<%= dynamic_image_tag(image) %>
|
|
200
|
+
<%= dynamic_image_tag(image, size: "400x400") %>
|
|
104
201
|
<%= dynamic_image_tag(image, size: "400x400", crop: true) %>
|
|
105
202
|
```
|
|
106
203
|
|
|
107
|
-
|
|
204
|
+
### Alt text
|
|
205
|
+
|
|
206
|
+
The `alt` attribute is resolved from `DynamicImage::Model#alt_text`. The
|
|
207
|
+
column isn't added by default, either create it yourself, or override the
|
|
208
|
+
method. Alternatively, pass the `alt` option to the helper.
|
|
108
209
|
|
|
109
210
|
```erb
|
|
110
|
-
<%= dynamic_image_tag(image,
|
|
211
|
+
<%= dynamic_image_tag(image, alt: "A description") %>
|
|
212
|
+
<%= dynamic_image_tag(image, alt: "") %>
|
|
111
213
|
```
|
|
112
214
|
|
|
113
|
-
|
|
114
|
-
|
|
215
|
+
### Responsive images
|
|
216
|
+
|
|
217
|
+
`dynamic_picture_tag` renders a `picture` element covering a range of
|
|
218
|
+
widths using `srcset`.
|
|
115
219
|
|
|
116
220
|
```erb
|
|
117
|
-
<%=
|
|
221
|
+
<%= dynamic_picture_tag(image, sizes: "50vw", alt: "A description") %>
|
|
118
222
|
```
|
|
119
223
|
|
|
120
|
-
|
|
224
|
+
```html
|
|
225
|
+
<picture>
|
|
226
|
+
<source type="image/webp"
|
|
227
|
+
srcset="/images/… 420w, /images/… 590w, /images/… 830w, …"
|
|
228
|
+
sizes="50vw">
|
|
229
|
+
<img src="/images/…/1200x800/….jpg" width="1200" height="800"
|
|
230
|
+
alt="A description">
|
|
231
|
+
</picture>
|
|
232
|
+
```
|
|
121
233
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
234
|
+
Note that there is no `size:` option here. Instead, pass a `ratio:`
|
|
235
|
+
when you want the image cropped. In addition to the `"16:9"` example
|
|
236
|
+
below, it also accepts rationals and floats.
|
|
125
237
|
|
|
126
|
-
|
|
127
|
-
|
|
238
|
+
```erb
|
|
239
|
+
<%= dynamic_picture_tag(image, ratio: "16:9", sizes: "50vw") %>
|
|
240
|
+
```
|
|
128
241
|
|
|
129
|
-
|
|
130
|
-
* [Rack::Cache](http://rtomayko.github.io/rack-cache/)
|
|
131
|
-
* [actionpack-page_caching](https://github.com/rails/actionpack-page_caching)
|
|
242
|
+
#### Image breakpoints
|
|
132
243
|
|
|
133
|
-
|
|
134
|
-
|
|
244
|
+
The `srcset` sizes are computed per image based on its own width,
|
|
245
|
+
stepping down geometrically in intervals, configured by `step:`.
|
|
246
|
+
The default configuration yields roughly 7 variants across the range,
|
|
247
|
+
depending on the original size.
|
|
135
248
|
|
|
136
|
-
|
|
249
|
+
```ruby
|
|
250
|
+
DynamicImage.default_breakpoints = 320..3200
|
|
251
|
+
DynamicImage.breakpoint_step = 1.4
|
|
252
|
+
DynamicImage.picture_fallback_width = 1200
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
If you'd rather have fixed widths, change the range to an array:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
DynamicImage.default_breakpoints = [400, 800, 1200]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
All three options can be overridden per call.
|
|
262
|
+
|
|
263
|
+
```erb
|
|
264
|
+
<%= dynamic_picture_tag(image, sizes: "50vw",
|
|
265
|
+
breakpoints: 320..1600, step: 1.25) %>
|
|
266
|
+
<%= dynamic_picture_tag(image, sizes: "50vw", breakpoints: [400, 800, 1200]) %>
|
|
267
|
+
<%= dynamic_picture_tag(logo, sizes: "120px", breakpoints: 240) %>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Media queries
|
|
271
|
+
|
|
272
|
+
`dynamic_picture_source_tag` renders a single `source`, so you can
|
|
273
|
+
compose a `picture` by hand when you want different crops depending on
|
|
274
|
+
media queries.
|
|
275
|
+
|
|
276
|
+
```erb
|
|
277
|
+
<picture>
|
|
278
|
+
<%= dynamic_picture_source_tag(image, ratio: "21:9",
|
|
279
|
+
media: "(min-width: 1000px)") %>
|
|
280
|
+
<%= dynamic_picture_source_tag(image, ratio: "1:1") %>
|
|
281
|
+
<%= dynamic_image_tag(image, size: "1200x1200", crop: true, alt: "…") %>
|
|
282
|
+
</picture>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Formats
|
|
137
286
|
|
|
138
|
-
|
|
287
|
+
Supported formats are JPEG, PNG, GIF, WebP, JPEG XL and TIFF.
|
|
288
|
+
BMP, HEIC and AVIF can also be uploaded, but aren't supported for output.
|
|
289
|
+
|
|
290
|
+
The preferred format lists are configurable, sorted most preferred format
|
|
291
|
+
first:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
DynamicImage.default_formats = %i[jpeg png gif webp]
|
|
295
|
+
DynamicImage.mailer_formats = %i[jpeg png gif]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Unless the source format matches the preferred formats, it will
|
|
299
|
+
be converted to the most appropriate format. For instance, a HEIC from
|
|
300
|
+
a phone will be served as either JPEG or PNG, depending on if it's
|
|
301
|
+
transparent or not.
|
|
302
|
+
|
|
303
|
+
Pass `format:` to override the defaults.
|
|
304
|
+
|
|
305
|
+
```erb
|
|
306
|
+
<%= dynamic_image_tag(image, size: "400x400", format: %i[jpeg png gif]) %>
|
|
307
|
+
<%= dynamic_image_tag(image, size: "400x400", format: :jxl) %>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
For consistent appearance, all images are converted to the sRGB colorspace.
|
|
311
|
+
Any embedded color profiles will be taken into account when doing so.
|
|
312
|
+
|
|
313
|
+
## Working with images
|
|
314
|
+
|
|
315
|
+
### Cropping
|
|
316
|
+
|
|
317
|
+
Images can be pre-cropped by setting `crop_width`, `crop_height`,
|
|
318
|
+
`crop_start_x` and `crop_start_y`. The crop is applied to every rendered
|
|
319
|
+
version of the image, except the ones served by the `original` and
|
|
320
|
+
`uncropped` actions.
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
image.update(
|
|
324
|
+
crop_start_x: 15,
|
|
325
|
+
crop_start_y: 20,
|
|
326
|
+
crop_width: 300,
|
|
327
|
+
crop_height: 200
|
|
328
|
+
)
|
|
329
|
+
image.size # => Vector2d(300, 200)
|
|
330
|
+
image.real_size # => Vector2d(500, 400)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
By default, images are cropped from the center. Set `crop_gravity_x` and
|
|
334
|
+
`crop_gravity_y` to set a different focal point. When cropping,
|
|
335
|
+
DynamicImage will attempt to keep this pixel as close to the center as
|
|
336
|
+
possible without zooming in.
|
|
337
|
+
|
|
338
|
+
The crop gravity is relative to the original image, so that the crop size
|
|
339
|
+
can change without moving the focal point.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
image.update(crop_gravity_x: 120, crop_gravity_y: 80)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Transforming the stored image
|
|
346
|
+
|
|
347
|
+
Rendering never modifies the stored file, but `rotate` and `resize` do.
|
|
348
|
+
Both write a new file and update the stored dimensions, and neither
|
|
349
|
+
saves the record for you.
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
image.rotate(90)
|
|
353
|
+
image.resize(Vector2d.new(800, 800))
|
|
354
|
+
image.save
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`rotate` turns the image, taking the crop along with it. The angle must
|
|
358
|
+
be a multiple of 90.
|
|
359
|
+
|
|
360
|
+
`resize` scales the stored file down and replaces the original.
|
|
361
|
+
|
|
362
|
+
### Outside of views
|
|
363
|
+
|
|
364
|
+
The helpers cover rendering in HTML, but the classes underneath are
|
|
365
|
+
public API and useful when you need dimensions or image data directly.
|
|
366
|
+
|
|
367
|
+
#### Calculating sizes
|
|
368
|
+
|
|
369
|
+
`DynamicImage::ImageSizing` can be used for size calculation.
|
|
370
|
+
It takes the same options as the helpers.
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
sizing = DynamicImage::ImageSizing.new(image) # a 1600x1000 image
|
|
374
|
+
|
|
375
|
+
sizing.fit("400x400") # => Vector2d(400.0, 250.0)
|
|
376
|
+
sizing.fit("400x400", crop: true) # => Vector2d(400.0, 400.0)
|
|
377
|
+
sizing.fit("2000x2000") # => Vector2d(1600.0, 1000.0)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Processing images
|
|
381
|
+
|
|
382
|
+
`DynamicImage::ProcessedImage` returns processed image data as a binary
|
|
383
|
+
string.
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
size = DynamicImage::ImageSizing.new(image).fit("800x800")
|
|
387
|
+
data = DynamicImage::ProcessedImage.new(image, format: :jpg)
|
|
388
|
+
.cropped_and_resized(size)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The result is stored as a variant, so that subsequent calls are cheap.
|
|
392
|
+
|
|
393
|
+
## How it works
|
|
394
|
+
|
|
395
|
+
### Signed URLs
|
|
396
|
+
|
|
397
|
+
All URLs are signed with a HMAC to protect against denial of service and
|
|
398
|
+
enumeration attacks.
|
|
399
|
+
|
|
400
|
+
The signing key is derived from your application's `secret_key_base`.
|
|
401
|
+
Take care if you rotate it, this will invalidate every image URL you
|
|
402
|
+
have generated. Plan for that if you rotate secrets.
|
|
403
|
+
|
|
404
|
+
### Caching
|
|
405
|
+
|
|
406
|
+
Generating images on the fly is expensive, so each processed size is
|
|
407
|
+
stored as a variant, a separate record with its data in Dis, and reused
|
|
408
|
+
on subsequent requests. Variants are discarded automatically when the
|
|
409
|
+
image is replaced.
|
|
410
|
+
|
|
411
|
+
Responses are served with a far-future `Cache-Control` header and
|
|
412
|
+
respect `If-Modified-Since`, so they play well with an HTTP cache in
|
|
413
|
+
front — [CloudFlare](https://www.cloudflare.com),
|
|
414
|
+
[Rack::Cache](http://rtomayko.github.io/rack-cache/) or
|
|
415
|
+
[actionpack-page_caching](https://github.com/rails/actionpack-page_caching),
|
|
416
|
+
to name a few. It's perfectly safe to cache images indefinitely: the URL
|
|
417
|
+
is timestamped, and will change if the object changes.
|
|
418
|
+
|
|
419
|
+
## Upgrading
|
|
420
|
+
|
|
421
|
+
DynamicImage can't migrate your image table for you: it's named whatever
|
|
422
|
+
you called it, and you may have several. When a release changes the
|
|
423
|
+
schema, you generate the migration. Most releases don't.
|
|
424
|
+
|
|
425
|
+
### 3.1
|
|
426
|
+
|
|
427
|
+
Uploaded WebP is now served as WebP instead of being converted to JPEG,
|
|
428
|
+
so URLs and cached variants for WebP images change. Mailer views still
|
|
429
|
+
get JPEG, PNG or GIF. Set `DynamicImage.default_formats` back to
|
|
430
|
+
`%i[jpeg png gif]` to keep the old behaviour.
|
|
431
|
+
|
|
432
|
+
This release also adds `frame_count` and `alpha`, and an index on
|
|
433
|
+
`content_hash`. Run this once per image model.
|
|
434
|
+
|
|
435
|
+
```sh
|
|
436
|
+
bin/rails generate dynamic_image:upgrade Image
|
|
437
|
+
bin/rails db:migrate
|
|
438
|
+
bin/rails dynamic_image:backfill MODELS=Image
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The backfill reads every image back from storage, so it takes a while on
|
|
442
|
+
a large library. It's safe to interrupt and re-run, and it leaves
|
|
443
|
+
`updated_at` alone, so existing URLs and cached variants stay valid.
|
|
444
|
+
|
|
445
|
+
Two things to watch on a large table. `add_index` locks the table while
|
|
446
|
+
it builds, so move it into its own migration with
|
|
447
|
+
`algorithm: :concurrently`. And if your table was created by an older
|
|
448
|
+
generator, its columns are all nullable and the generator will list the
|
|
449
|
+
ones that should be `NOT NULL`; correcting them is optional and left to
|
|
450
|
+
you, since `change_column_null` fails if any row holds a `NULL`.
|
|
451
|
+
|
|
452
|
+
## Documentation
|
|
453
|
+
|
|
454
|
+
See the [generated documentation on RubyDoc.info](https://www.rubydoc.info/gems/dynamic_image),
|
|
455
|
+
and the [changelog](CHANGELOG.md) for release notes.
|
|
456
|
+
|
|
457
|
+
## Contributing
|
|
458
|
+
|
|
459
|
+
Bug reports and pull requests are welcome on
|
|
460
|
+
[GitHub](https://github.com/elektronaut/dynamic_image). See
|
|
461
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests and how
|
|
462
|
+
commits are formatted, and note that this project ships with a
|
|
463
|
+
[code of conduct](CODE_OF_CONDUCT.md).
|
|
464
|
+
|
|
465
|
+
## License
|
|
139
466
|
|
|
140
|
-
|
|
141
|
-
[MIT License](http://www.opensource.org/licenses/MIT).
|
|
467
|
+
Released under the [MIT License](MIT-LICENSE).
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DynamicImage
|
|
4
|
+
# = DynamicImage Variant
|
|
5
|
+
#
|
|
6
|
+
# A cached rendering of an image at one size, crop and format. {DynamicImage::ProcessedImage} creates variants on
|
|
7
|
+
# demand and destroys them when the image they belong to changes or is destroyed, so there is rarely a reason to
|
|
8
|
+
# work with them directly.
|
|
9
|
+
#
|
|
10
|
+
# Data is stored in Dis under its own type, keeping it separate from the originals.
|
|
11
|
+
#
|
|
12
|
+
# @see DynamicImage::ProcessedImage
|
|
13
|
+
# @see DynamicImage::Model::Variants
|
|
4
14
|
class Variant < ApplicationRecord
|
|
5
15
|
include Dis::Model
|
|
6
16
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicImage
|
|
4
|
+
# = DynamicImage Backfill
|
|
5
|
+
#
|
|
6
|
+
# Fills in the metadata columns for records stored before those columns existed. The values are read back from
|
|
7
|
+
# the stored file.
|
|
8
|
+
#
|
|
9
|
+
# Records are written with +update_columns+, so no callbacks run and +updated_at+ is left alone.
|
|
10
|
+
# {DynamicImage::Model#to_param} fingerprints on +updated_at+, and touching it would invalidate every image URL
|
|
11
|
+
# and every cached variant.
|
|
12
|
+
#
|
|
13
|
+
# DynamicImage::Backfill.new(Image).run
|
|
14
|
+
#
|
|
15
|
+
# @see DynamicImage::Schema
|
|
16
|
+
class Backfill
|
|
17
|
+
# The columns this fills in.
|
|
18
|
+
COLUMNS = %i[frame_count alpha].freeze
|
|
19
|
+
|
|
20
|
+
# @return [Class] the model being backfilled
|
|
21
|
+
attr_reader :model
|
|
22
|
+
|
|
23
|
+
# @return [Integer] records written
|
|
24
|
+
attr_reader :updated
|
|
25
|
+
|
|
26
|
+
# @return [Integer] records left alone, unreadable or missing
|
|
27
|
+
attr_reader :skipped
|
|
28
|
+
|
|
29
|
+
# @param model [Class] a model including {DynamicImage::Model}
|
|
30
|
+
def initialize(model)
|
|
31
|
+
@model = model
|
|
32
|
+
@updated = 0
|
|
33
|
+
@skipped = 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The records with a column still unset.
|
|
37
|
+
#
|
|
38
|
+
# @return [ActiveRecord::Relation] the pending records
|
|
39
|
+
def pending
|
|
40
|
+
COLUMNS.map { |column| model.where(column => nil) }.reduce(:or)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reads metadata for every pending record and fills the columns in.
|
|
44
|
+
#
|
|
45
|
+
# @yieldparam record [DynamicImage::Model] each record, after it has been processed
|
|
46
|
+
# @return [self]
|
|
47
|
+
# @raise [ArgumentError] if the table doesn't have the columns yet
|
|
48
|
+
def run
|
|
49
|
+
ensure_columns
|
|
50
|
+
pending.find_each do |record|
|
|
51
|
+
process(record)
|
|
52
|
+
yield(record) if block_given?
|
|
53
|
+
end
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def ensure_columns
|
|
60
|
+
missing = COLUMNS.reject { |c| model.column_names.include?(c.to_s) }
|
|
61
|
+
return if missing.empty?
|
|
62
|
+
|
|
63
|
+
raise ArgumentError,
|
|
64
|
+
"#{model.table_name} has no #{missing.join(', ')} column. " \
|
|
65
|
+
"Run bin/rails generate dynamic_image:upgrade #{model.name}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process(record)
|
|
69
|
+
record.with_data_file { |path| apply(record, path) }
|
|
70
|
+
rescue Dis::Errors::NotFoundError
|
|
71
|
+
@skipped += 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Metadata reads lazily, so the values have to be resolved before the file goes out of scope. Written with
|
|
75
|
+
# update_columns so that no callbacks run.
|
|
76
|
+
def apply(record, path)
|
|
77
|
+
metadata = DynamicImage::Metadata.new(path)
|
|
78
|
+
return @skipped += 1 unless metadata.valid?
|
|
79
|
+
|
|
80
|
+
record.update_columns(frame_count: metadata.frame_count,
|
|
81
|
+
alpha: metadata.alpha?)
|
|
82
|
+
@updated += 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -3,10 +3,32 @@
|
|
|
3
3
|
module DynamicImage
|
|
4
4
|
# = DynamicImage Belongs To
|
|
5
5
|
#
|
|
6
|
+
# Extends ActiveRecord with {ClassMethods#belongs_to_image}, an association that accepts an uploaded file in place
|
|
7
|
+
# of a record.
|
|
8
|
+
#
|
|
9
|
+
# The engine mixes this into +ActiveRecord::Base+, so it is available in any model.
|
|
6
10
|
module BelongsTo
|
|
7
11
|
extend ActiveSupport::Concern
|
|
8
12
|
|
|
9
13
|
module ClassMethods
|
|
14
|
+
# Declares an association to an image. Behaves like +belongs_to+ and takes the same arguments, with one
|
|
15
|
+
# addition: assigning anything that isn't a {DynamicImage::Model} builds the associated record from it,
|
|
16
|
+
# treating it as an uploaded file. A file can therefore be posted straight to the parent model.
|
|
17
|
+
#
|
|
18
|
+
# The image is built, not saved, and is written when the parent is. Add +validates_associated+ if an invalid
|
|
19
|
+
# upload should invalidate the parent; without it the assignment is silently dropped on save.
|
|
20
|
+
#
|
|
21
|
+
# @param name [Symbol] the name of the association
|
|
22
|
+
# @param scope [Proc, nil] an optional scope, as +belongs_to+
|
|
23
|
+
# @return [void]
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# class User < ActiveRecord::Base
|
|
27
|
+
# belongs_to_image :avatar, class_name: "Image"
|
|
28
|
+
# validates_associated :avatar
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# User.create(avatar: params[:file])
|
|
10
32
|
def belongs_to_image(name, scope = nil, **)
|
|
11
33
|
belongs_to(name, scope, **)
|
|
12
34
|
|