imgwire 0.1.0 → 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 +4 -4
- data/CODEGEN_VERSION +1 -1
- data/README.md +30 -0
- data/generated/lib/imgwire-generated/models/image_schema.rb +53 -1
- data/generated/lib/imgwire-generated/models/supported_mime_type.rb +6 -1
- data/lib/imgwire/image.rb +3 -87
- data/lib/imgwire/url_transformations.rb +907 -0
- data/lib/imgwire/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51b2aefb2be738f37e07af70399bd781d56bdeacc886b4fdd86335ac2c52539f
|
|
4
|
+
data.tar.gz: e60ce39c3b7825ab9cbcb632073839c5343cd667b3dbd6c5f3f6c6b25b522b44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51e0d57435f845cf12f0b3b677e75ab32fae572d1c2ee5b347c35cee54f964bdae11d9a974dd1f210fa3b09cedc9057332afb77e2ee01cef62a0a60c6d11547d
|
|
7
|
+
data.tar.gz: 18bc491397f7fd0f0433abd0e9d645327ebd5437fe8365ac80eaf699f9c741a6403dd28a11b1731c6b408889eb6a157f7da20d83f3810c8dce10c379530fc720
|
data/CODEGEN_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
b57e439d6e9c3859c7bddb889658b5ff808503ccc136ea28a56e0fd57a1678aa
|
data/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# `imgwire`
|
|
2
4
|
|
|
3
5
|
[](https://rubygems.org/gems/imgwire)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
7
|
[](https://github.com/Blackhawk-Software/imgwire-ruby/actions/workflows/ci.yml)
|
|
5
8
|
[](https://github.com/Blackhawk-Software/imgwire-ruby/actions/workflows/release.yml)
|
|
6
9
|
|
|
@@ -8,6 +11,9 @@
|
|
|
8
11
|
|
|
9
12
|
Use it in Rails apps, workers, jobs, and other backend runtimes to authenticate with a Server API Key, upload files from Ruby IO objects, manage server-side resources, and generate image transformation URLs without rebuilding imgwire request plumbing yourself.
|
|
10
13
|
|
|
14
|
+
> [!TIP]
|
|
15
|
+
> Obtain an API key by signing up at [imgwire.dev](https://imgwire.dev). Read the full API & SDK documentation [here](https://docs.imgwire.dev/guides/backend-quickstart).
|
|
16
|
+
|
|
11
17
|
## Installation
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -114,6 +120,30 @@ puts image.url(
|
|
|
114
120
|
)
|
|
115
121
|
```
|
|
116
122
|
|
|
123
|
+
The helper accepts the transform names and aliases from
|
|
124
|
+
`docs/imgwire-url-transformations-guide.md`. Multi-field transforms can be
|
|
125
|
+
passed as their URL string syntax or as Ruby hashes:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
puts image.url(
|
|
129
|
+
gradient: {
|
|
130
|
+
colors: ["#0b1f5e", "#ff2a2a"],
|
|
131
|
+
angle: 90,
|
|
132
|
+
opacity: 0.25,
|
|
133
|
+
blend: "overlay"
|
|
134
|
+
},
|
|
135
|
+
watermark_url: "https://example.com/logo.png",
|
|
136
|
+
watermark_position: {
|
|
137
|
+
gravity: "southeast",
|
|
138
|
+
x: -24,
|
|
139
|
+
y: -24,
|
|
140
|
+
opacity: 0.85
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Out-of-range transform values are omitted from the generated URL.
|
|
146
|
+
|
|
117
147
|
## Generation
|
|
118
148
|
|
|
119
149
|
From a clean checkout:
|
|
@@ -15,6 +15,8 @@ require 'time'
|
|
|
15
15
|
|
|
16
16
|
module ImgwireGenerated
|
|
17
17
|
class ImageSchema
|
|
18
|
+
attr_accessor :can_upload
|
|
19
|
+
|
|
18
20
|
attr_accessor :cdn_url
|
|
19
21
|
|
|
20
22
|
attr_accessor :created_at
|
|
@@ -37,6 +39,8 @@ module ImgwireGenerated
|
|
|
37
39
|
|
|
38
40
|
attr_accessor :idempotency_key
|
|
39
41
|
|
|
42
|
+
attr_accessor :is_directly_deliverable
|
|
43
|
+
|
|
40
44
|
attr_accessor :mime_type
|
|
41
45
|
|
|
42
46
|
attr_accessor :original_filename
|
|
@@ -80,6 +84,7 @@ module ImgwireGenerated
|
|
|
80
84
|
# Attribute mapping from ruby-style variable name to JSON key.
|
|
81
85
|
def self.attribute_map
|
|
82
86
|
{
|
|
87
|
+
:'can_upload' => :'can_upload',
|
|
83
88
|
:'cdn_url' => :'cdn_url',
|
|
84
89
|
:'created_at' => :'created_at',
|
|
85
90
|
:'custom_metadata' => :'custom_metadata',
|
|
@@ -91,6 +96,7 @@ module ImgwireGenerated
|
|
|
91
96
|
:'height' => :'height',
|
|
92
97
|
:'id' => :'id',
|
|
93
98
|
:'idempotency_key' => :'idempotency_key',
|
|
99
|
+
:'is_directly_deliverable' => :'is_directly_deliverable',
|
|
94
100
|
:'mime_type' => :'mime_type',
|
|
95
101
|
:'original_filename' => :'original_filename',
|
|
96
102
|
:'processed_metadata_at' => :'processed_metadata_at',
|
|
@@ -116,6 +122,7 @@ module ImgwireGenerated
|
|
|
116
122
|
# Attribute type mapping.
|
|
117
123
|
def self.openapi_types
|
|
118
124
|
{
|
|
125
|
+
:'can_upload' => :'Boolean',
|
|
119
126
|
:'cdn_url' => :'String',
|
|
120
127
|
:'created_at' => :'Time',
|
|
121
128
|
:'custom_metadata' => :'Hash<String, CustomMetadataValue>',
|
|
@@ -127,6 +134,7 @@ module ImgwireGenerated
|
|
|
127
134
|
:'height' => :'Integer',
|
|
128
135
|
:'id' => :'String',
|
|
129
136
|
:'idempotency_key' => :'String',
|
|
137
|
+
:'is_directly_deliverable' => :'Boolean',
|
|
130
138
|
:'mime_type' => :'SupportedMimeType',
|
|
131
139
|
:'original_filename' => :'String',
|
|
132
140
|
:'processed_metadata_at' => :'Time',
|
|
@@ -168,6 +176,12 @@ module ImgwireGenerated
|
|
|
168
176
|
h[k.to_sym] = v
|
|
169
177
|
}
|
|
170
178
|
|
|
179
|
+
if attributes.key?(:'can_upload')
|
|
180
|
+
self.can_upload = attributes[:'can_upload']
|
|
181
|
+
else
|
|
182
|
+
self.can_upload = nil
|
|
183
|
+
end
|
|
184
|
+
|
|
171
185
|
if attributes.key?(:'cdn_url')
|
|
172
186
|
self.cdn_url = attributes[:'cdn_url']
|
|
173
187
|
else
|
|
@@ -238,6 +252,12 @@ module ImgwireGenerated
|
|
|
238
252
|
self.idempotency_key = nil
|
|
239
253
|
end
|
|
240
254
|
|
|
255
|
+
if attributes.key?(:'is_directly_deliverable')
|
|
256
|
+
self.is_directly_deliverable = attributes[:'is_directly_deliverable']
|
|
257
|
+
else
|
|
258
|
+
self.is_directly_deliverable = nil
|
|
259
|
+
end
|
|
260
|
+
|
|
241
261
|
if attributes.key?(:'mime_type')
|
|
242
262
|
self.mime_type = attributes[:'mime_type']
|
|
243
263
|
else
|
|
@@ -298,6 +318,10 @@ module ImgwireGenerated
|
|
|
298
318
|
def list_invalid_properties
|
|
299
319
|
warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
|
|
300
320
|
invalid_properties = Array.new
|
|
321
|
+
if @can_upload.nil?
|
|
322
|
+
invalid_properties.push('invalid value for "can_upload", can_upload cannot be nil.')
|
|
323
|
+
end
|
|
324
|
+
|
|
301
325
|
if @cdn_url.nil?
|
|
302
326
|
invalid_properties.push('invalid value for "cdn_url", cdn_url cannot be nil.')
|
|
303
327
|
end
|
|
@@ -326,6 +350,10 @@ module ImgwireGenerated
|
|
|
326
350
|
invalid_properties.push('invalid value for "id", id cannot be nil.')
|
|
327
351
|
end
|
|
328
352
|
|
|
353
|
+
if @is_directly_deliverable.nil?
|
|
354
|
+
invalid_properties.push('invalid value for "is_directly_deliverable", is_directly_deliverable cannot be nil.')
|
|
355
|
+
end
|
|
356
|
+
|
|
329
357
|
if @mime_type.nil?
|
|
330
358
|
invalid_properties.push('invalid value for "mime_type", mime_type cannot be nil.')
|
|
331
359
|
end
|
|
@@ -357,6 +385,7 @@ module ImgwireGenerated
|
|
|
357
385
|
# @return true if the model is valid
|
|
358
386
|
def valid?
|
|
359
387
|
warn '[DEPRECATED] the `valid?` method is obsolete'
|
|
388
|
+
return false if @can_upload.nil?
|
|
360
389
|
return false if @cdn_url.nil?
|
|
361
390
|
return false if @created_at.nil?
|
|
362
391
|
return false if @custom_metadata.nil?
|
|
@@ -364,6 +393,7 @@ module ImgwireGenerated
|
|
|
364
393
|
return false if @extension.nil?
|
|
365
394
|
return false if @height.nil?
|
|
366
395
|
return false if @id.nil?
|
|
396
|
+
return false if @is_directly_deliverable.nil?
|
|
367
397
|
return false if @mime_type.nil?
|
|
368
398
|
return false if @original_filename.nil?
|
|
369
399
|
return false if @size_bytes.nil?
|
|
@@ -373,6 +403,16 @@ module ImgwireGenerated
|
|
|
373
403
|
true
|
|
374
404
|
end
|
|
375
405
|
|
|
406
|
+
# Custom attribute writer method with validation
|
|
407
|
+
# @param [Object] can_upload Value to be assigned
|
|
408
|
+
def can_upload=(can_upload)
|
|
409
|
+
if can_upload.nil?
|
|
410
|
+
fail ArgumentError, 'can_upload cannot be nil'
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
@can_upload = can_upload
|
|
414
|
+
end
|
|
415
|
+
|
|
376
416
|
# Custom attribute writer method with validation
|
|
377
417
|
# @param [Object] cdn_url Value to be assigned
|
|
378
418
|
def cdn_url=(cdn_url)
|
|
@@ -443,6 +483,16 @@ module ImgwireGenerated
|
|
|
443
483
|
@id = id
|
|
444
484
|
end
|
|
445
485
|
|
|
486
|
+
# Custom attribute writer method with validation
|
|
487
|
+
# @param [Object] is_directly_deliverable Value to be assigned
|
|
488
|
+
def is_directly_deliverable=(is_directly_deliverable)
|
|
489
|
+
if is_directly_deliverable.nil?
|
|
490
|
+
fail ArgumentError, 'is_directly_deliverable cannot be nil'
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
@is_directly_deliverable = is_directly_deliverable
|
|
494
|
+
end
|
|
495
|
+
|
|
446
496
|
# Custom attribute writer method with validation
|
|
447
497
|
# @param [Object] mime_type Value to be assigned
|
|
448
498
|
def mime_type=(mime_type)
|
|
@@ -508,6 +558,7 @@ module ImgwireGenerated
|
|
|
508
558
|
def ==(o)
|
|
509
559
|
return true if self.equal?(o)
|
|
510
560
|
self.class == o.class &&
|
|
561
|
+
can_upload == o.can_upload &&
|
|
511
562
|
cdn_url == o.cdn_url &&
|
|
512
563
|
created_at == o.created_at &&
|
|
513
564
|
custom_metadata == o.custom_metadata &&
|
|
@@ -519,6 +570,7 @@ module ImgwireGenerated
|
|
|
519
570
|
height == o.height &&
|
|
520
571
|
id == o.id &&
|
|
521
572
|
idempotency_key == o.idempotency_key &&
|
|
573
|
+
is_directly_deliverable == o.is_directly_deliverable &&
|
|
522
574
|
mime_type == o.mime_type &&
|
|
523
575
|
original_filename == o.original_filename &&
|
|
524
576
|
processed_metadata_at == o.processed_metadata_at &&
|
|
@@ -539,7 +591,7 @@ module ImgwireGenerated
|
|
|
539
591
|
# Calculates hash code according to all attributes.
|
|
540
592
|
# @return [Integer] Hash code
|
|
541
593
|
def hash
|
|
542
|
-
[cdn_url, created_at, custom_metadata, deleted_at, environment_id, exif_data, extension, hash_sha256, height, id, idempotency_key, mime_type, original_filename, processed_metadata_at, purpose, size_bytes, status, updated_at, upload_token_id, width].hash
|
|
594
|
+
[can_upload, cdn_url, created_at, custom_metadata, deleted_at, environment_id, exif_data, extension, hash_sha256, height, id, idempotency_key, is_directly_deliverable, mime_type, original_filename, processed_metadata_at, purpose, size_bytes, status, updated_at, upload_token_id, width].hash
|
|
543
595
|
end
|
|
544
596
|
|
|
545
597
|
# Builds the object from hash
|
|
@@ -16,13 +16,18 @@ require 'time'
|
|
|
16
16
|
module ImgwireGenerated
|
|
17
17
|
class SupportedMimeType
|
|
18
18
|
IMAGE_JPEG = "image/jpeg".freeze
|
|
19
|
+
IMAGE_JXL = "image/jxl".freeze
|
|
19
20
|
IMAGE_PNG = "image/png".freeze
|
|
20
21
|
IMAGE_WEBP = "image/webp".freeze
|
|
21
22
|
IMAGE_AVIF = "image/avif".freeze
|
|
22
23
|
IMAGE_GIF = "image/gif".freeze
|
|
24
|
+
IMAGE_VND_MICROSOFT_ICON = "image/vnd.microsoft.icon".freeze
|
|
25
|
+
IMAGE_HEIC = "image/heic".freeze
|
|
26
|
+
IMAGE_BMP = "image/bmp".freeze
|
|
27
|
+
IMAGE_TIFF = "image/tiff".freeze
|
|
23
28
|
|
|
24
29
|
def self.all_vars
|
|
25
|
-
@all_vars ||= [IMAGE_JPEG, IMAGE_PNG, IMAGE_WEBP, IMAGE_AVIF, IMAGE_GIF].freeze
|
|
30
|
+
@all_vars ||= [IMAGE_JPEG, IMAGE_JXL, IMAGE_PNG, IMAGE_WEBP, IMAGE_AVIF, IMAGE_GIF, IMAGE_VND_MICROSOFT_ICON, IMAGE_HEIC, IMAGE_BMP, IMAGE_TIFF].freeze
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
# Builds the enum from string
|
data/lib/imgwire/image.rb
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'uri'
|
|
4
|
+
require 'imgwire/url_transformations'
|
|
4
5
|
|
|
5
6
|
module Imgwire
|
|
6
7
|
class Image < ImgwireGenerated::ImageSchema
|
|
7
8
|
PRESETS = %w[thumbnail small medium large].freeze
|
|
8
|
-
ROTATE_ANGLES = [0, 90, 180, 270, 360].freeze
|
|
9
|
-
FORMATS = %w[jpg png avif gif webp].freeze
|
|
10
|
-
|
|
11
|
-
RULES = {
|
|
12
|
-
'background' => %w[background bg],
|
|
13
|
-
'crop' => %w[crop],
|
|
14
|
-
'enlarge' => %w[enlarge],
|
|
15
|
-
'format' => %w[format fm],
|
|
16
|
-
'gravity' => %w[gravity],
|
|
17
|
-
'height' => %w[height h],
|
|
18
|
-
'quality' => %w[quality q],
|
|
19
|
-
'rotate' => %w[rotate rot],
|
|
20
|
-
'strip_metadata' => %w[strip_metadata strip],
|
|
21
|
-
'width' => %w[width w]
|
|
22
|
-
}.freeze
|
|
23
9
|
|
|
24
10
|
def self.wrap(value)
|
|
25
11
|
return value if value.is_a?(self)
|
|
@@ -38,7 +24,7 @@ module Imgwire
|
|
|
38
24
|
def url(options = {})
|
|
39
25
|
options = symbolize_keys(options)
|
|
40
26
|
path = build_preset_path(options[:preset])
|
|
41
|
-
query = build_query(options.except(:preset))
|
|
27
|
+
query = Imgwire::URLTransformations.build_query(options.except(:preset))
|
|
42
28
|
|
|
43
29
|
uri = URI.parse(cdn_url)
|
|
44
30
|
uri.path = path
|
|
@@ -61,77 +47,7 @@ module Imgwire
|
|
|
61
47
|
raise ArgumentError, 'Invalid transformation rule value for preset' unless PRESETS.include?(preset)
|
|
62
48
|
|
|
63
49
|
uri = URI.parse(cdn_url)
|
|
64
|
-
|
|
65
|
-
prefix = slash_index ? uri.path[0..slash_index] : ''
|
|
66
|
-
file_name = slash_index ? uri.path[(slash_index + 1)..] : uri.path
|
|
67
|
-
dot_index = file_name.rindex('.')
|
|
68
|
-
raise ArgumentError, 'Cannot apply a preset to a CDN URL without a file extension.' if dot_index.nil?
|
|
69
|
-
|
|
70
|
-
"#{prefix}#{file_name}@#{preset}"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def build_query(options)
|
|
74
|
-
present = []
|
|
75
|
-
|
|
76
|
-
RULES.each do |canonical, aliases|
|
|
77
|
-
matches = aliases.filter_map do |name|
|
|
78
|
-
symbol = name.to_sym
|
|
79
|
-
[symbol, options[symbol]] if options.key?(symbol)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
next if matches.empty?
|
|
83
|
-
raise ArgumentError, "Duplicate transformation rule: #{canonical}" if matches.length > 1
|
|
84
|
-
|
|
85
|
-
value = normalize_rule(canonical, matches.first.last)
|
|
86
|
-
present << [canonical, value] unless value.nil?
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
present
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def normalize_rule(canonical, value)
|
|
93
|
-
case canonical
|
|
94
|
-
when 'background'
|
|
95
|
-
string = value.to_s.delete_prefix('#')
|
|
96
|
-
unless string.match?(/\A[\da-fA-F]{6}\z/)
|
|
97
|
-
raise ArgumentError,
|
|
98
|
-
"Invalid transformation rule value for #{canonical}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
string.downcase
|
|
102
|
-
when 'crop', 'gravity'
|
|
103
|
-
value.to_s
|
|
104
|
-
when 'enlarge', 'strip_metadata'
|
|
105
|
-
value ? 'true' : nil
|
|
106
|
-
when 'format'
|
|
107
|
-
string = value.to_s
|
|
108
|
-
unless FORMATS.include?(string)
|
|
109
|
-
raise ArgumentError,
|
|
110
|
-
"Invalid transformation rule value for #{canonical}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
string
|
|
114
|
-
when 'height', 'quality', 'width'
|
|
115
|
-
integer = Integer(value)
|
|
116
|
-
unless integer.positive?
|
|
117
|
-
raise ArgumentError,
|
|
118
|
-
"Invalid transformation rule value for #{canonical}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
integer.to_s
|
|
122
|
-
when 'rotate'
|
|
123
|
-
integer = Integer(value)
|
|
124
|
-
unless ROTATE_ANGLES.include?(integer)
|
|
125
|
-
raise ArgumentError,
|
|
126
|
-
"Invalid transformation rule value for #{canonical}"
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
integer.to_s
|
|
130
|
-
else
|
|
131
|
-
raise ArgumentError, "Unsupported transformation rule: #{canonical}"
|
|
132
|
-
end
|
|
133
|
-
rescue ArgumentError, TypeError
|
|
134
|
-
raise ArgumentError, "Invalid transformation rule value for #{canonical}"
|
|
50
|
+
"#{uri.path}@#{preset}"
|
|
135
51
|
end
|
|
136
52
|
end
|
|
137
53
|
end
|
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Imgwire
|
|
8
|
+
# rubocop:disable Metrics/ModuleLength
|
|
9
|
+
module URLTransformations
|
|
10
|
+
RULES = {
|
|
11
|
+
'adjust' => %w[a adjust],
|
|
12
|
+
'background' => %w[bg background],
|
|
13
|
+
'background_alpha' => %w[bga background_alpha],
|
|
14
|
+
'blur' => %w[bl blur],
|
|
15
|
+
'brightness' => %w[br brightness],
|
|
16
|
+
'color_profile' => %w[cp icc color_profile],
|
|
17
|
+
'colorize' => %w[col colorize],
|
|
18
|
+
'contrast' => %w[co contrast],
|
|
19
|
+
'crop' => %w[c crop],
|
|
20
|
+
'dpi' => %w[dpi],
|
|
21
|
+
'dpr' => %w[dpr],
|
|
22
|
+
'duotone' => %w[dt duotone],
|
|
23
|
+
'enlarge' => %w[el enlarge],
|
|
24
|
+
'extend' => %w[ex extend],
|
|
25
|
+
'extend_aspect_ratio' => %w[exar extend_ar extend_aspect_ratio],
|
|
26
|
+
'flip' => %w[fl flip],
|
|
27
|
+
'format' => %w[f format ext extension fm],
|
|
28
|
+
'gradient' => %w[gr gradient],
|
|
29
|
+
'gravity' => %w[g gravity],
|
|
30
|
+
'height' => %w[h height],
|
|
31
|
+
'hue' => %w[hu hue],
|
|
32
|
+
'keep_copyright' => %w[kcr keep_copyright],
|
|
33
|
+
'lightness' => %w[l lightness],
|
|
34
|
+
'min-height' => %w[mh min_height min-height],
|
|
35
|
+
'min-width' => %w[mw min_width min-width],
|
|
36
|
+
'monochrome' => %w[mc monochrome],
|
|
37
|
+
'negate' => %w[neg negate],
|
|
38
|
+
'normalize' => %w[norm normalise normalize],
|
|
39
|
+
'padding' => %w[pd padding],
|
|
40
|
+
'pixelate' => %w[pix pixelate],
|
|
41
|
+
'quality' => %w[q quality],
|
|
42
|
+
'resizing_algorithm' => %w[ra resizing_algorithm],
|
|
43
|
+
'resizing_type' => %w[resizing_type],
|
|
44
|
+
'rotate' => %w[rot rotate],
|
|
45
|
+
'saturation' => %w[sa saturation],
|
|
46
|
+
'sharpen' => %w[sh sharpen],
|
|
47
|
+
'strip_color_profile' => %w[scp strip_color_profile],
|
|
48
|
+
'strip_metadata' => %w[sm strip_metadata strip],
|
|
49
|
+
'watermark' => %w[wm watermark],
|
|
50
|
+
'watermark_position' => %w[wmp watermark_offset watermark_position],
|
|
51
|
+
'watermark_rotate' => %w[wmr wm_rot watermark_rotate],
|
|
52
|
+
'watermark_shadow' => %w[wmsh watermark_shadow],
|
|
53
|
+
'watermark_size' => %w[wms watermark_size],
|
|
54
|
+
'watermark_text' => %w[wmt watermark_text],
|
|
55
|
+
'watermark_url' => %w[wmu watermark_url],
|
|
56
|
+
'width' => %w[w width],
|
|
57
|
+
'zoom' => %w[z zoom]
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
COLOR_PROFILES = %w[srgb rgb16 cmyk keep preserve].freeze
|
|
61
|
+
FORMATS = %w[auto jpg jpeg png webp avif gif tiff].freeze
|
|
62
|
+
FLIP_VALUES = %w[vertical horizontal both].freeze
|
|
63
|
+
RESIZING_ALGORITHMS = %w[nearest cubic mitchell lanczos2 lanczos3].freeze
|
|
64
|
+
RESIZING_TYPES = %w[cover contain fill inside outside].freeze
|
|
65
|
+
RESIZING_TYPE_ALIASES = {
|
|
66
|
+
'auto' => 'inside',
|
|
67
|
+
'fill-down' => 'inside',
|
|
68
|
+
'fit' => 'inside',
|
|
69
|
+
'force' => 'fill'
|
|
70
|
+
}.freeze
|
|
71
|
+
GRAVITY_VALUES = %w[
|
|
72
|
+
ce center north south east west northeast northwest southeast southwest attention entropy
|
|
73
|
+
].freeze
|
|
74
|
+
GRAVITY_ALIASES = {
|
|
75
|
+
'n' => 'north',
|
|
76
|
+
'no' => 'north',
|
|
77
|
+
's' => 'south',
|
|
78
|
+
'so' => 'south',
|
|
79
|
+
'e' => 'east',
|
|
80
|
+
'ea' => 'east',
|
|
81
|
+
'w' => 'west',
|
|
82
|
+
'we' => 'west',
|
|
83
|
+
'ne' => 'northeast',
|
|
84
|
+
'noea' => 'northeast',
|
|
85
|
+
'se' => 'southeast',
|
|
86
|
+
'soea' => 'southeast',
|
|
87
|
+
'nw' => 'northwest',
|
|
88
|
+
'nowe' => 'northwest',
|
|
89
|
+
'sw' => 'southwest',
|
|
90
|
+
'sowe' => 'southwest',
|
|
91
|
+
'ce:sm' => 'attention'
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
def build_query(options)
|
|
97
|
+
normalized_options = normalize_top_level_keys(options)
|
|
98
|
+
|
|
99
|
+
RULES.each_with_object([]) do |(canonical, aliases), present|
|
|
100
|
+
matches = aliases.filter_map do |name|
|
|
101
|
+
key = name.to_sym
|
|
102
|
+
[key, normalized_options[key]] if normalized_options.key?(key)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
next if matches.empty?
|
|
106
|
+
raise ArgumentError, "Duplicate transformation rule: #{canonical}" if matches.length > 1
|
|
107
|
+
|
|
108
|
+
value = normalize_rule(canonical, matches.first.last)
|
|
109
|
+
present << [canonical, value] unless value.nil?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def normalize_rule(canonical, value)
|
|
114
|
+
case canonical
|
|
115
|
+
when 'adjust'
|
|
116
|
+
normalize_adjust(value)
|
|
117
|
+
when 'background', 'colorize'
|
|
118
|
+
normalize_color(value)
|
|
119
|
+
when 'background_alpha'
|
|
120
|
+
normalize_number(value, min: 0, max: 1)
|
|
121
|
+
when 'blur'
|
|
122
|
+
normalize_true_or_number(value, min: 0.3, max: 100)
|
|
123
|
+
when 'brightness', 'lightness', 'saturation'
|
|
124
|
+
normalize_number(value, min: 0.01, max: 10)
|
|
125
|
+
when 'color_profile'
|
|
126
|
+
normalize_enum(value, COLOR_PROFILES)
|
|
127
|
+
when 'contrast'
|
|
128
|
+
normalize_contrast(value)
|
|
129
|
+
when 'crop'
|
|
130
|
+
normalize_crop(value)
|
|
131
|
+
when 'dpi'
|
|
132
|
+
normalize_integer(value, min: 1, max: 600)
|
|
133
|
+
when 'dpr'
|
|
134
|
+
normalize_number(value, min: 0.01, max: 8)
|
|
135
|
+
when 'duotone'
|
|
136
|
+
normalize_duotone(value)
|
|
137
|
+
when 'enlarge', 'keep_copyright', 'strip_color_profile', 'strip_metadata'
|
|
138
|
+
normalize_boolean_flag(value)
|
|
139
|
+
when 'extend'
|
|
140
|
+
normalize_extend(value)
|
|
141
|
+
when 'extend_aspect_ratio'
|
|
142
|
+
normalize_extend_aspect_ratio(value)
|
|
143
|
+
when 'flip'
|
|
144
|
+
normalize_flip(value)
|
|
145
|
+
when 'format'
|
|
146
|
+
normalize_enum(value, FORMATS)
|
|
147
|
+
when 'gradient'
|
|
148
|
+
normalize_gradient(value)
|
|
149
|
+
when 'gravity'
|
|
150
|
+
normalize_gravity(value)
|
|
151
|
+
when 'height', 'min-height', 'min-width', 'width'
|
|
152
|
+
normalize_integer(value, min: 1, max: 8192)
|
|
153
|
+
when 'hue'
|
|
154
|
+
normalize_number(value)
|
|
155
|
+
when 'monochrome'
|
|
156
|
+
normalize_monochrome(value)
|
|
157
|
+
when 'negate'
|
|
158
|
+
normalize_negate(value)
|
|
159
|
+
when 'normalize'
|
|
160
|
+
normalize_normalize(value)
|
|
161
|
+
when 'padding'
|
|
162
|
+
normalize_padding(value)
|
|
163
|
+
when 'pixelate'
|
|
164
|
+
normalize_integer(value, min: 2, max: 256)
|
|
165
|
+
when 'quality'
|
|
166
|
+
normalize_integer(value, min: 1, max: 100)
|
|
167
|
+
when 'resizing_algorithm'
|
|
168
|
+
normalize_enum(value, RESIZING_ALGORITHMS)
|
|
169
|
+
when 'resizing_type'
|
|
170
|
+
normalize_enum(value, RESIZING_TYPES, aliases: RESIZING_TYPE_ALIASES)
|
|
171
|
+
when 'rotate', 'watermark_rotate'
|
|
172
|
+
normalize_rotate(value)
|
|
173
|
+
when 'sharpen'
|
|
174
|
+
normalize_sharpen(value)
|
|
175
|
+
when 'watermark'
|
|
176
|
+
normalize_watermark(value)
|
|
177
|
+
when 'watermark_position'
|
|
178
|
+
normalize_watermark_position(value)
|
|
179
|
+
when 'watermark_shadow'
|
|
180
|
+
normalize_watermark_shadow(value)
|
|
181
|
+
when 'watermark_size'
|
|
182
|
+
normalize_watermark_size(value)
|
|
183
|
+
when 'watermark_text'
|
|
184
|
+
normalize_text_or_json_object(value)
|
|
185
|
+
when 'watermark_url'
|
|
186
|
+
normalize_watermark_url(value)
|
|
187
|
+
when 'zoom'
|
|
188
|
+
normalize_zoom(value)
|
|
189
|
+
end
|
|
190
|
+
rescue ArgumentError, TypeError
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def normalize_adjust(value)
|
|
195
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
196
|
+
|
|
197
|
+
hash = value_hash(value)
|
|
198
|
+
if hash
|
|
199
|
+
brightness = normalize_number(field(hash, 'brightness'))
|
|
200
|
+
saturation = normalize_number(field(hash, 'saturation'))
|
|
201
|
+
color = normalize_number(field(hash, 'color'))
|
|
202
|
+
|
|
203
|
+
return contiguous_segments([brightness], [saturation, color])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
segments = split_segments(value, min: 1, max: 3)
|
|
207
|
+
return unless segments
|
|
208
|
+
|
|
209
|
+
brightness = normalize_number(segments[0])
|
|
210
|
+
saturation = normalize_number(segments[1]) if segments.length > 1
|
|
211
|
+
color = normalize_number(segments[2]) if segments.length > 2
|
|
212
|
+
|
|
213
|
+
contiguous_segments([brightness], [saturation, color].first(segments.length - 1))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def normalize_contrast(value)
|
|
217
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
218
|
+
|
|
219
|
+
hash = value_hash(value)
|
|
220
|
+
if hash
|
|
221
|
+
multiplier = normalize_number(field(hash, 'multiplier'))
|
|
222
|
+
pivot = normalize_number(field(hash, 'pivot'))
|
|
223
|
+
|
|
224
|
+
return contiguous_segments([multiplier], [pivot])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
segments = split_segments(value, min: 1, max: 2)
|
|
228
|
+
return unless segments
|
|
229
|
+
|
|
230
|
+
multiplier = normalize_number(segments[0])
|
|
231
|
+
pivot = normalize_number(segments[1]) if segments.length > 1
|
|
232
|
+
|
|
233
|
+
contiguous_segments([multiplier], [pivot].first(segments.length - 1))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def normalize_crop(value)
|
|
237
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
238
|
+
|
|
239
|
+
hash = value_hash(value)
|
|
240
|
+
return normalize_crop_hash(hash) if hash
|
|
241
|
+
|
|
242
|
+
segments = split_segments(value, min: 2, max: 5)
|
|
243
|
+
return unless [2, 3, 4, 5].include?(segments&.length)
|
|
244
|
+
|
|
245
|
+
if segments.length <= 3
|
|
246
|
+
width = normalize_integer(segments[0], min: 1)
|
|
247
|
+
height = normalize_integer(segments[1], min: 1)
|
|
248
|
+
gravity = normalize_gravity(segments[2]) if segments.length == 3
|
|
249
|
+
|
|
250
|
+
return contiguous_segments([width, height], [gravity].first(segments.length - 2))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
x = normalize_integer(segments[0])
|
|
254
|
+
y = normalize_integer(segments[1])
|
|
255
|
+
width = normalize_integer(segments[2], min: 1)
|
|
256
|
+
height = normalize_integer(segments[3], min: 1)
|
|
257
|
+
gravity = normalize_gravity(segments[4]) if segments.length == 5
|
|
258
|
+
|
|
259
|
+
contiguous_segments([x, y, width, height], [gravity].first(segments.length - 4))
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def normalize_crop_hash(hash)
|
|
263
|
+
x = normalize_integer(field(hash, 'x'))
|
|
264
|
+
y = normalize_integer(field(hash, 'y'))
|
|
265
|
+
width = normalize_integer(field(hash, 'width', 'w'), min: 1)
|
|
266
|
+
height = normalize_integer(field(hash, 'height', 'h'), min: 1)
|
|
267
|
+
gravity = normalize_gravity(field(hash, 'gravity', 'g'))
|
|
268
|
+
|
|
269
|
+
if field?(hash, 'x') || field?(hash, 'y')
|
|
270
|
+
contiguous_segments([x, y, width, height], [gravity])
|
|
271
|
+
else
|
|
272
|
+
contiguous_segments([width, height], [gravity])
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def normalize_duotone(value)
|
|
277
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
278
|
+
|
|
279
|
+
hash = value_hash(value)
|
|
280
|
+
if hash
|
|
281
|
+
shadow = normalize_color(field(hash, 'shadowColor', 'shadow_color', 'shadow'))
|
|
282
|
+
highlight = normalize_color(field(hash, 'highlightColor', 'highlight_color', 'highlight'))
|
|
283
|
+
|
|
284
|
+
return contiguous_segments([shadow, highlight], [])
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
segments = split_segments(value, min: 2, max: 2)
|
|
288
|
+
return unless segments
|
|
289
|
+
|
|
290
|
+
shadow = normalize_color(segments[0])
|
|
291
|
+
highlight = normalize_color(segments[1])
|
|
292
|
+
|
|
293
|
+
contiguous_segments([shadow, highlight], [])
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def normalize_extend(value)
|
|
297
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
298
|
+
|
|
299
|
+
hash = value_hash(value)
|
|
300
|
+
return normalize_extend_hash(hash) if hash
|
|
301
|
+
|
|
302
|
+
segments = split_segments(value, min: 1, max: 5)
|
|
303
|
+
return unless segments
|
|
304
|
+
|
|
305
|
+
top = normalize_integer(segments[0])
|
|
306
|
+
right = normalize_integer(segments[1]) if segments.length > 1
|
|
307
|
+
bottom = normalize_integer(segments[2]) if segments.length > 2
|
|
308
|
+
left = normalize_integer(segments[3]) if segments.length > 3
|
|
309
|
+
background = normalize_color(segments[4]) if segments.length > 4
|
|
310
|
+
|
|
311
|
+
contiguous_segments([top], [right, bottom, left, background].first(segments.length - 1))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def normalize_extend_hash(hash)
|
|
315
|
+
top = normalize_integer(field(hash, 'top'))
|
|
316
|
+
right = normalize_integer(field(hash, 'right'))
|
|
317
|
+
bottom = normalize_integer(field(hash, 'bottom'))
|
|
318
|
+
left = normalize_integer(field(hash, 'left'))
|
|
319
|
+
background = normalize_color(field(hash, 'background', 'bg'))
|
|
320
|
+
|
|
321
|
+
compact_json = compact_json_object(
|
|
322
|
+
hash,
|
|
323
|
+
'top' => top,
|
|
324
|
+
'right' => right,
|
|
325
|
+
'bottom' => bottom,
|
|
326
|
+
'left' => left,
|
|
327
|
+
'background' => background
|
|
328
|
+
)
|
|
329
|
+
return unless compact_json
|
|
330
|
+
|
|
331
|
+
contiguous_segments([top], [right, bottom, left, background]) || compact_json
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def normalize_extend_aspect_ratio(value)
|
|
335
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
336
|
+
|
|
337
|
+
hash = value_hash(value)
|
|
338
|
+
if hash
|
|
339
|
+
ratio = normalize_number(field(hash, 'ratio'), min: 0.000001)
|
|
340
|
+
return ratio if ratio
|
|
341
|
+
|
|
342
|
+
width = normalize_integer(field(hash, 'width', 'w'), min: 1)
|
|
343
|
+
height = normalize_integer(field(hash, 'height', 'h'), min: 1)
|
|
344
|
+
|
|
345
|
+
return contiguous_segments([width, height], [])
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
segments = split_segments(value, min: 1, max: 2)
|
|
349
|
+
return unless segments
|
|
350
|
+
|
|
351
|
+
return normalize_number(segments[0], min: 0.000001) if segments.length == 1
|
|
352
|
+
|
|
353
|
+
width = normalize_integer(segments[0], min: 1)
|
|
354
|
+
height = normalize_integer(segments[1], min: 1)
|
|
355
|
+
|
|
356
|
+
contiguous_segments([width, height], [])
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def normalize_flip(value)
|
|
360
|
+
hash = value_hash(value)
|
|
361
|
+
if hash
|
|
362
|
+
horizontal = parse_boolean(field(hash, 'horizontal'))
|
|
363
|
+
vertical = parse_boolean(field(hash, 'vertical'))
|
|
364
|
+
return if horizontal.nil? && vertical.nil?
|
|
365
|
+
|
|
366
|
+
return 'both' if horizontal && vertical
|
|
367
|
+
return 'horizontal' if horizontal
|
|
368
|
+
return 'vertical' if vertical
|
|
369
|
+
|
|
370
|
+
return
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
string = normalize_string(value)
|
|
374
|
+
return string if FLIP_VALUES.include?(string)
|
|
375
|
+
|
|
376
|
+
segments = split_segments(value, min: 2, max: 2)
|
|
377
|
+
return unless segments
|
|
378
|
+
|
|
379
|
+
horizontal = parse_boolean(segments[0])
|
|
380
|
+
vertical = parse_boolean(segments[1])
|
|
381
|
+
return if horizontal.nil? || vertical.nil?
|
|
382
|
+
|
|
383
|
+
"#{horizontal}:#{vertical}"
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def normalize_gradient(value)
|
|
387
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
388
|
+
|
|
389
|
+
hash = value_hash(value)
|
|
390
|
+
return normalize_gradient_hash(hash) if hash
|
|
391
|
+
|
|
392
|
+
segments = split_segments(value, min: 1, max: 4)
|
|
393
|
+
return unless segments
|
|
394
|
+
|
|
395
|
+
colors = normalize_color_list(segments[0])
|
|
396
|
+
angle = normalize_number(segments[1]) if segments.length > 1
|
|
397
|
+
opacity = normalize_number(segments[2], min: 0, max: 1) if segments.length > 2
|
|
398
|
+
blend = normalize_non_empty_string(segments[3]) if segments.length > 3
|
|
399
|
+
|
|
400
|
+
contiguous_segments([colors], [angle, opacity, blend].first(segments.length - 1))
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def normalize_gradient_hash(hash)
|
|
404
|
+
colors = normalize_color_list(field(hash, 'colors'))
|
|
405
|
+
angle = normalize_number(field(hash, 'angle'))
|
|
406
|
+
opacity = normalize_number(field(hash, 'opacity'), min: 0, max: 1)
|
|
407
|
+
blend = normalize_non_empty_string(field(hash, 'blend'))
|
|
408
|
+
|
|
409
|
+
contiguous_segments([colors], [angle, opacity, blend])
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def normalize_monochrome(value)
|
|
413
|
+
boolean = parse_boolean(value)
|
|
414
|
+
return 'true' if boolean == true
|
|
415
|
+
return if boolean == false
|
|
416
|
+
|
|
417
|
+
normalize_color(value)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def normalize_negate(value)
|
|
421
|
+
hash = value_hash(value)
|
|
422
|
+
if hash
|
|
423
|
+
alpha = parse_boolean(field(hash, 'alpha'))
|
|
424
|
+
return unless [true, false].include?(alpha)
|
|
425
|
+
|
|
426
|
+
return "alpha:#{alpha}"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
boolean = parse_boolean(value)
|
|
430
|
+
return 'true' if boolean == true
|
|
431
|
+
return if boolean == false
|
|
432
|
+
|
|
433
|
+
segments = split_segments(value, min: 2, max: 2)
|
|
434
|
+
return unless segments&.first == 'alpha'
|
|
435
|
+
|
|
436
|
+
alpha = parse_boolean(segments[1])
|
|
437
|
+
return unless [true, false].include?(alpha)
|
|
438
|
+
|
|
439
|
+
"alpha:#{alpha}"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def normalize_normalize(value)
|
|
443
|
+
hash = value_hash(value)
|
|
444
|
+
if hash
|
|
445
|
+
lower = normalize_number(field(hash, 'lower'))
|
|
446
|
+
upper = normalize_number(field(hash, 'upper'))
|
|
447
|
+
|
|
448
|
+
return contiguous_segments([lower, upper], [])
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
boolean = parse_boolean(value)
|
|
452
|
+
return 'true' if boolean == true
|
|
453
|
+
return if boolean == false
|
|
454
|
+
|
|
455
|
+
segments = split_segments(value, min: 2, max: 2)
|
|
456
|
+
return unless segments
|
|
457
|
+
|
|
458
|
+
lower = normalize_number(segments[0])
|
|
459
|
+
upper = normalize_number(segments[1])
|
|
460
|
+
|
|
461
|
+
contiguous_segments([lower, upper], [])
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def normalize_padding(value)
|
|
465
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
466
|
+
|
|
467
|
+
hash = value_hash(value)
|
|
468
|
+
return normalize_padding_hash(hash) if hash
|
|
469
|
+
|
|
470
|
+
segments = split_segments(value, min: 1, max: 4)
|
|
471
|
+
return unless segments
|
|
472
|
+
|
|
473
|
+
normalized = segments.map { |segment| normalize_integer(segment) }
|
|
474
|
+
return if normalized.any?(&:nil?)
|
|
475
|
+
|
|
476
|
+
normalized.join(':')
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def normalize_padding_hash(hash)
|
|
480
|
+
all = normalize_integer(field(hash, 'all'))
|
|
481
|
+
return all if all
|
|
482
|
+
|
|
483
|
+
x = normalize_integer(field(hash, 'x'))
|
|
484
|
+
y = normalize_integer(field(hash, 'y'))
|
|
485
|
+
return contiguous_segments([x, y], []) if x || y
|
|
486
|
+
|
|
487
|
+
top = normalize_integer(field(hash, 'top'))
|
|
488
|
+
right = normalize_integer(field(hash, 'right'))
|
|
489
|
+
bottom = normalize_integer(field(hash, 'bottom'))
|
|
490
|
+
left = normalize_integer(field(hash, 'left'))
|
|
491
|
+
|
|
492
|
+
compact_json = compact_json_object(
|
|
493
|
+
hash,
|
|
494
|
+
'top' => top,
|
|
495
|
+
'right' => right,
|
|
496
|
+
'bottom' => bottom,
|
|
497
|
+
'left' => left
|
|
498
|
+
)
|
|
499
|
+
return unless compact_json
|
|
500
|
+
|
|
501
|
+
contiguous_segments([top, right, bottom, left], []) || compact_json
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def normalize_rotate(value)
|
|
505
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
506
|
+
|
|
507
|
+
hash = value_hash(value)
|
|
508
|
+
if hash
|
|
509
|
+
angle = normalize_number(field(hash, 'angle'))
|
|
510
|
+
background = normalize_color(field(hash, 'background', 'bg'))
|
|
511
|
+
|
|
512
|
+
return contiguous_segments([angle], [background])
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
segments = split_segments(value, min: 1, max: 2)
|
|
516
|
+
return unless segments
|
|
517
|
+
|
|
518
|
+
angle = normalize_number(segments[0])
|
|
519
|
+
background = normalize_color(segments[1]) if segments.length > 1
|
|
520
|
+
|
|
521
|
+
contiguous_segments([angle], [background].first(segments.length - 1))
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def normalize_sharpen(value)
|
|
525
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
526
|
+
|
|
527
|
+
hash = value_hash(value)
|
|
528
|
+
return json_object(hash) if hash
|
|
529
|
+
|
|
530
|
+
normalize_true_or_number(value)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def normalize_watermark(value)
|
|
534
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
535
|
+
|
|
536
|
+
hash = value_hash(value)
|
|
537
|
+
return normalize_watermark_hash(hash) if hash
|
|
538
|
+
|
|
539
|
+
segments = split_segments(value, min: 1, max: 4)
|
|
540
|
+
return unless segments
|
|
541
|
+
|
|
542
|
+
image_id = normalize_non_empty_string(segments[0])
|
|
543
|
+
gravity = normalize_gravity(segments[1]) if segments.length > 1
|
|
544
|
+
x = normalize_integer(segments[2]) if segments.length > 2
|
|
545
|
+
y = normalize_integer(segments[3]) if segments.length > 3
|
|
546
|
+
|
|
547
|
+
contiguous_segments([image_id], [gravity, x, y].first(segments.length - 1))
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def normalize_watermark_hash(hash)
|
|
551
|
+
image_id = normalize_non_empty_string(field(hash, 'image_id', 'imageId', 'id'))
|
|
552
|
+
gravity = normalize_gravity(field(hash, 'gravity', 'g'))
|
|
553
|
+
x = normalize_integer(field(hash, 'x'))
|
|
554
|
+
y = normalize_integer(field(hash, 'y'))
|
|
555
|
+
|
|
556
|
+
contiguous_segments([image_id], [gravity, x, y])
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def normalize_watermark_position(value)
|
|
560
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
561
|
+
|
|
562
|
+
hash = value_hash(value)
|
|
563
|
+
return normalize_watermark_position_hash(hash) if hash
|
|
564
|
+
|
|
565
|
+
segments = split_segments(value, min: 1, max: 5)
|
|
566
|
+
return unless segments
|
|
567
|
+
|
|
568
|
+
gravity = normalize_gravity(segments[0])
|
|
569
|
+
x = normalize_integer(segments[1]) if segments.length > 1
|
|
570
|
+
y = normalize_integer(segments[2]) if segments.length > 2
|
|
571
|
+
opacity = normalize_number(segments[3], min: 0, max: 1) if segments.length > 3
|
|
572
|
+
blend = normalize_non_empty_string(segments[4]) if segments.length > 4
|
|
573
|
+
|
|
574
|
+
contiguous_segments([gravity], [x, y, opacity, blend].first(segments.length - 1))
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def normalize_watermark_position_hash(hash)
|
|
578
|
+
gravity = normalize_gravity(field(hash, 'gravity', 'g'))
|
|
579
|
+
x = normalize_integer(field(hash, 'x'))
|
|
580
|
+
y = normalize_integer(field(hash, 'y'))
|
|
581
|
+
opacity = normalize_number(field(hash, 'opacity'), min: 0, max: 1)
|
|
582
|
+
blend = normalize_non_empty_string(field(hash, 'blend'))
|
|
583
|
+
|
|
584
|
+
contiguous_segments([gravity], [x, y, opacity, blend])
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def normalize_watermark_shadow(value)
|
|
588
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
589
|
+
|
|
590
|
+
boolean = parse_boolean(value)
|
|
591
|
+
return 'true' if boolean == true
|
|
592
|
+
return if boolean == false
|
|
593
|
+
|
|
594
|
+
hash = value_hash(value)
|
|
595
|
+
return normalize_watermark_shadow_hash(hash) if hash
|
|
596
|
+
|
|
597
|
+
segments = split_segments(value, min: 1, max: 4)
|
|
598
|
+
return unless segments
|
|
599
|
+
|
|
600
|
+
color = normalize_color(segments[0])
|
|
601
|
+
blur = normalize_number(segments[1]) if segments.length > 1
|
|
602
|
+
x = normalize_integer(segments[2]) if segments.length > 2
|
|
603
|
+
y = normalize_integer(segments[3]) if segments.length > 3
|
|
604
|
+
|
|
605
|
+
contiguous_segments([color], [blur, x, y].first(segments.length - 1))
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def normalize_watermark_shadow_hash(hash)
|
|
609
|
+
color = normalize_color(field(hash, 'color'))
|
|
610
|
+
blur = normalize_number(field(hash, 'blur'))
|
|
611
|
+
x = normalize_integer(field(hash, 'x'))
|
|
612
|
+
y = normalize_integer(field(hash, 'y'))
|
|
613
|
+
|
|
614
|
+
contiguous_segments([color], [blur, x, y])
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def normalize_watermark_size(value)
|
|
618
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
619
|
+
|
|
620
|
+
hash = value_hash(value)
|
|
621
|
+
return normalize_watermark_size_hash(hash) if hash
|
|
622
|
+
|
|
623
|
+
segments = split_segments(value, min: 1, max: 3)
|
|
624
|
+
return unless segments
|
|
625
|
+
|
|
626
|
+
width = normalize_integer(segments[0], min: 1)
|
|
627
|
+
height = normalize_integer(segments[1], min: 1) if segments.length > 1
|
|
628
|
+
scale = normalize_number(segments[2], min: 0.000001) if segments.length > 2
|
|
629
|
+
|
|
630
|
+
contiguous_segments([width], [height, scale].first(segments.length - 1))
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def normalize_watermark_size_hash(hash)
|
|
634
|
+
width = normalize_integer(field(hash, 'width', 'w'), min: 1)
|
|
635
|
+
height = normalize_integer(field(hash, 'height', 'h'), min: 1)
|
|
636
|
+
scale = normalize_number(field(hash, 'scale'), min: 0.000001)
|
|
637
|
+
|
|
638
|
+
contiguous_segments([width], [height, scale])
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def normalize_text_or_json_object(value)
|
|
642
|
+
return normalize_json_string(value) if json_object_string?(value)
|
|
643
|
+
|
|
644
|
+
hash = value_hash(value)
|
|
645
|
+
return json_object(hash) if hash
|
|
646
|
+
|
|
647
|
+
normalize_non_empty_string(value)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def normalize_watermark_url(value)
|
|
651
|
+
string = normalize_non_empty_string(value)
|
|
652
|
+
return unless string
|
|
653
|
+
|
|
654
|
+
return Base64.strict_encode64(string) if valid_https_url?(string)
|
|
655
|
+
|
|
656
|
+
string if valid_base64_https_url?(string)
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def normalize_zoom(value)
|
|
660
|
+
hash = value_hash(value)
|
|
661
|
+
if hash
|
|
662
|
+
factor = normalize_number(field(hash, 'factor'), min: 0.000001)
|
|
663
|
+
gravity = normalize_gravity(field(hash, 'gravity', 'g'))
|
|
664
|
+
|
|
665
|
+
return contiguous_segments([factor], [gravity])
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
segments = split_segments(value, min: 1, max: 2)
|
|
669
|
+
return unless segments
|
|
670
|
+
|
|
671
|
+
factor = normalize_number(segments[0], min: 0.000001)
|
|
672
|
+
gravity = normalize_gravity(segments[1]) if segments.length > 1
|
|
673
|
+
|
|
674
|
+
contiguous_segments([factor], [gravity].first(segments.length - 1))
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def normalize_top_level_keys(hash)
|
|
678
|
+
hash.to_h.transform_keys(&:to_sym)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def value_hash(value)
|
|
682
|
+
return unless value.respond_to?(:to_hash)
|
|
683
|
+
|
|
684
|
+
value.to_hash.transform_keys(&:to_s)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def field(hash, *names)
|
|
688
|
+
names.each do |name|
|
|
689
|
+
return hash[name] if hash.key?(name)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
nil
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def field?(hash, *names)
|
|
696
|
+
names.any? { |name| hash.key?(name) }
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def split_segments(value, min:, max:)
|
|
700
|
+
string = normalize_non_empty_string(value)
|
|
701
|
+
return unless string
|
|
702
|
+
|
|
703
|
+
segments = string.split(':', -1)
|
|
704
|
+
return unless segments.length.between?(min, max)
|
|
705
|
+
return if segments.any?(&:empty?)
|
|
706
|
+
|
|
707
|
+
segments
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def contiguous_segments(required, optional)
|
|
711
|
+
return if required.any?(&:nil?)
|
|
712
|
+
|
|
713
|
+
last_present = optional.rindex { |segment| !segment.nil? }
|
|
714
|
+
selected_optional = last_present.nil? ? [] : optional[0..last_present]
|
|
715
|
+
return if selected_optional.any?(&:nil?)
|
|
716
|
+
|
|
717
|
+
(required + selected_optional).join(':')
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def normalize_boolean_flag(value)
|
|
721
|
+
parse_boolean(value) == true ? 'true' : nil
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def parse_boolean(value)
|
|
725
|
+
return true if value == true
|
|
726
|
+
return false if value == false
|
|
727
|
+
return true if value == 1
|
|
728
|
+
return false if value.is_a?(Numeric) && value.zero?
|
|
729
|
+
|
|
730
|
+
case normalize_string(value)
|
|
731
|
+
when 'true', '1'
|
|
732
|
+
true
|
|
733
|
+
when 'false', '0'
|
|
734
|
+
false
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def normalize_true_or_number(value, min: nil, max: nil)
|
|
739
|
+
boolean = parse_boolean(value)
|
|
740
|
+
return 'true' if boolean == true
|
|
741
|
+
return if boolean == false
|
|
742
|
+
|
|
743
|
+
normalize_number(value, min: min, max: max)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def normalize_integer(value, min: nil, max: nil)
|
|
747
|
+
integer = parse_integer(value)
|
|
748
|
+
return if integer.nil?
|
|
749
|
+
return if min && integer < min
|
|
750
|
+
return if max && integer > max
|
|
751
|
+
|
|
752
|
+
integer.to_s
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def parse_integer(value)
|
|
756
|
+
return value if value.is_a?(Integer)
|
|
757
|
+
|
|
758
|
+
if value.is_a?(Float)
|
|
759
|
+
return unless value.finite? && value == value.to_i
|
|
760
|
+
|
|
761
|
+
return value.to_i
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
string = normalize_non_empty_string(value)
|
|
765
|
+
return unless string&.match?(/\A[+-]?\d+\z/)
|
|
766
|
+
|
|
767
|
+
Integer(string)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def normalize_number(value, min: nil, max: nil)
|
|
771
|
+
number = parse_number(value)
|
|
772
|
+
return if number.nil?
|
|
773
|
+
return if min && number < min
|
|
774
|
+
return if max && number > max
|
|
775
|
+
|
|
776
|
+
format_number(number)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def parse_number(value)
|
|
780
|
+
return value.to_f if value.is_a?(Numeric) && value.to_f.finite?
|
|
781
|
+
|
|
782
|
+
string = normalize_non_empty_string(value)
|
|
783
|
+
return unless string
|
|
784
|
+
|
|
785
|
+
number = Float(string)
|
|
786
|
+
return unless number.finite?
|
|
787
|
+
|
|
788
|
+
number
|
|
789
|
+
rescue ArgumentError, TypeError
|
|
790
|
+
nil
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def format_number(number)
|
|
794
|
+
return number.to_i.to_s if number == number.to_i
|
|
795
|
+
|
|
796
|
+
number.to_s
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def normalize_enum(value, values, aliases: {})
|
|
800
|
+
string = normalize_string(value)
|
|
801
|
+
return aliases[string] if aliases.key?(string)
|
|
802
|
+
|
|
803
|
+
string if values.include?(string)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def normalize_gravity(value)
|
|
807
|
+
string = normalize_string(value)
|
|
808
|
+
return GRAVITY_ALIASES[string] if GRAVITY_ALIASES.key?(string)
|
|
809
|
+
|
|
810
|
+
string if GRAVITY_VALUES.include?(string)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def normalize_color(value)
|
|
814
|
+
string = normalize_non_empty_string(value)
|
|
815
|
+
return unless string
|
|
816
|
+
|
|
817
|
+
string = string.delete_prefix('#')
|
|
818
|
+
return string.downcase if string.match?(/\A(?:[[:xdigit:]]{3}|[[:xdigit:]]{6}|[[:xdigit:]]{8})\z/)
|
|
819
|
+
|
|
820
|
+
string if rgb_color?(string)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def rgb_color?(string)
|
|
824
|
+
segments = string.split(':', -1)
|
|
825
|
+
return false unless [3, 4].include?(segments.length)
|
|
826
|
+
|
|
827
|
+
red, green, blue = segments.first(3).map { |segment| parse_integer(segment) }
|
|
828
|
+
return false unless [red, green, blue].all? { |part| part&.between?(0, 255) }
|
|
829
|
+
return true if segments.length == 3
|
|
830
|
+
|
|
831
|
+
alpha = parse_number(segments[3])
|
|
832
|
+
alpha&.between?(0, 1)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def normalize_color_list(value)
|
|
836
|
+
colors = value.is_a?(Array) ? value : normalize_non_empty_string(value)&.split(',', -1)
|
|
837
|
+
return unless colors&.length&.>= 2
|
|
838
|
+
|
|
839
|
+
normalized = colors.map { |color| normalize_color(color) }
|
|
840
|
+
return if normalized.any?(&:nil?)
|
|
841
|
+
|
|
842
|
+
normalized.join(',')
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def normalize_non_empty_string(value)
|
|
846
|
+
string = value.to_s.strip
|
|
847
|
+
return if string.empty?
|
|
848
|
+
|
|
849
|
+
string
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def normalize_string(value)
|
|
853
|
+
normalize_non_empty_string(value)&.downcase
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def json_object_string?(value)
|
|
857
|
+
string = normalize_non_empty_string(value)
|
|
858
|
+
return false unless string&.start_with?('{')
|
|
859
|
+
|
|
860
|
+
JSON.parse(string).is_a?(Hash)
|
|
861
|
+
rescue JSON::ParserError
|
|
862
|
+
false
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def normalize_json_string(value)
|
|
866
|
+
normalize_non_empty_string(value)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def compact_json_object(input_hash, fields)
|
|
870
|
+
return unless fields.values.any?
|
|
871
|
+
return if fields.any? { |key, item| field?(input_hash, key) && item.nil? }
|
|
872
|
+
|
|
873
|
+
JSON.generate(fields.compact)
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def json_object(hash)
|
|
877
|
+
return if hash.empty?
|
|
878
|
+
|
|
879
|
+
JSON.generate(camelize_hash_keys(hash))
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def camelize_hash_keys(hash)
|
|
883
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
884
|
+
result[camelize_key(key)] = value.is_a?(Hash) ? camelize_hash_keys(value) : value
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def camelize_key(key)
|
|
889
|
+
key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def valid_https_url?(value)
|
|
893
|
+
uri = URI.parse(value)
|
|
894
|
+
uri.is_a?(URI::HTTPS) && uri.host && !uri.host.empty?
|
|
895
|
+
rescue URI::InvalidURIError
|
|
896
|
+
false
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def valid_base64_https_url?(value)
|
|
900
|
+
decoded = Base64.strict_decode64(value)
|
|
901
|
+
valid_https_url?(decoded)
|
|
902
|
+
rescue ArgumentError
|
|
903
|
+
false
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
# rubocop:enable Metrics/ModuleLength
|
|
907
|
+
end
|
data/lib/imgwire/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: imgwire
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blackhawk Software, LLC
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: typhoeus
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: base64
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.2'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rubocop
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -130,6 +144,7 @@ files:
|
|
|
130
144
|
- lib/imgwire/resources/images_resource.rb
|
|
131
145
|
- lib/imgwire/resources/metrics_resource.rb
|
|
132
146
|
- lib/imgwire/uploads.rb
|
|
147
|
+
- lib/imgwire/url_transformations.rb
|
|
133
148
|
- lib/imgwire/version.rb
|
|
134
149
|
homepage: https://github.com/Blackhawk-Software/imgwire-ruby
|
|
135
150
|
licenses:
|