Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to pass in custom widths #55

Merged
merged 7 commits into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ https://your-subdomain.imgix.net/images/demo.png?w=7400&s=a5dd7dda1dbac613f0475f
https://your-subdomain.imgix.net/images/demo.png?w=8192&s=9fbd257c53e770e345ce3412b64a3452 8192w
```

**Fixed image rendering**
### Fixed image rendering

In cases where enough information is provided about an image's dimensions, `to_srcset` will instead build a `srcset` that will allow for an image to be served at different resolutions. The parameters taken into consideration when determining if an image is fixed-width are `w`, `h`, and `ar`. By invoking `to_srcset` with either a width **or** the height and aspect ratio (along with `fit=crop`, typically) provided, a different `srcset` will be generated for a fixed-size image instead.

Expand All @@ -91,7 +91,28 @@ https://your-subdomain.imgix.net/images/demo.png?h=800&ar=3%3A2&fit=crop&dpr=5&s

For more information to better understand `srcset`, we highly recommend [Eric Portis' "Srcset and sizes" article](https://ericportis.com/posts/2014/srcset-sizes/) which goes into depth about the subject.

**Width Tolerance**
### Custom Widths

In situations where specific widths are desired when generating `srcset` pairs, a user can specify them by passing an array of integers to the `widths` keyword argument.

```rb
@client ||= Imgix::Client.new(host: 'testing.imgix.net', include_library_param: false)
.path('image.jpg')
.to_srcset(widths: [100, 500, 1000, 1800])
```

Will generate the following `srcset` of width pairs:

```html
https://testing.imgix.net/image.jpg?w=100 100w,
https://testing.imgix.net/image.jpg?w=500 500w,
https://testing.imgix.net/image.jpg?w=1000 1000w,
https://testing.imgix.net/image.jpg?w=1800 1800w
```

Please note that in situations where a `srcset` is being rendered as a [fixed image](#fixed-image-rendering), any custom `widths` passed in will be ignored. Additionally, if both `widths` and a `width_tolerance` are passed to the `to_srcset` method, the custom widths list will take precedence.

### Width Tolerance

The `srcset` width tolerance dictates the maximum tolerated size difference between an image's downloaded size and its rendered size. For example: setting this value to 0.1 means that an image will not render more than 10% larger or smaller than its native size. In practice, the image URLs generated for a width-based srcset attribute will grow by twice this rate. A lower tolerance means images will render closer to their native size (thereby reducing rendering artifacts), but a large srcset list will be generated and consequently users may experience lower rates of cache-hit for pre-rendered images on your site.

Expand Down
29 changes: 22 additions & 7 deletions lib/imgix/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def method_missing(method, *args, &block)
end
end

def to_srcset(width_tolerance: DEFAULT_WIDTH_TOLERANCE, **params)
def to_srcset(widths: [], width_tolerance: DEFAULT_WIDTH_TOLERANCE, **params)
prev_options = @options.dup
@options.merge!(params)

Expand All @@ -95,7 +95,7 @@ def to_srcset(width_tolerance: DEFAULT_WIDTH_TOLERANCE, **params)
if ((width) || (height && aspect_ratio))
srcset = build_dpr_srcset(@options)
else
srcset = build_srcset_pairs(width_tolerance: width_tolerance, params: @options)
srcset = build_srcset_pairs(widths: widths, width_tolerance: width_tolerance, params: @options)
end

@options = prev_options
Expand Down Expand Up @@ -128,16 +128,19 @@ def has_query?
query.length > 0
end

def build_srcset_pairs(width_tolerance:, params:)
def build_srcset_pairs(widths:, width_tolerance:, params:)
srcset = ''

unless width_tolerance == DEFAULT_WIDTH_TOLERANCE
widths = TARGET_WIDTHS.call(width_tolerance)
if !widths.empty?
validate_widths!(widths)
srcset_widths = widths
elsif width_tolerance != DEFAULT_WIDTH_TOLERANCE
srcset_widths = TARGET_WIDTHS.call(width_tolerance)
else
widths = @target_widths
srcset_widths = @target_widths
end

for width in widths do
for width in srcset_widths do
params['w'.to_sym] = width
srcset += "#{to_url(params)} #{width}w,\n"
end
Expand All @@ -156,5 +159,17 @@ def build_dpr_srcset(params)

srcset[0..-3]
end

def validate_widths!(widths)
unless widths.is_a? Array
raise ArgumentError, "The widths argument must be passed a valid array of integers"
else
valid_integers = widths.all? {|i| i.is_a?(Integer) }
unless valid_integers
raise ArgumentError, "A custom widths array must only contain integer values"
end
end
end

end
end
82 changes: 76 additions & 6 deletions test/units/srcset_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def test_no_parameters
expected_number_of_pairs = 31
assert_equal expected_number_of_pairs, srcset.split(',').length
end

def test_srcset_pair_values
resolutions = [100, 116, 134, 156, 182, 210, 244, 282,
328, 380, 442, 512, 594, 688, 798, 926,
Expand Down Expand Up @@ -67,7 +67,7 @@ def test_srcset_signs_urls
assert_equal expected_signature, generated_signature
}
end

private
def srcset
@client ||= Imgix::Client.new(host: 'testing.imgix.net', secure_url_token: 'MYT0KEN', include_library_param: false).path('image.jpg').to_srcset(w:100)
Expand Down Expand Up @@ -186,7 +186,7 @@ def test_srcset_signs_urls

signature_base = 'MYT0KEN' + '/image.jpg' + params;
expected_signature = Digest::MD5.hexdigest(signature_base)

assert_equal expected_signature, generated_signature
}
end
Expand Down Expand Up @@ -303,7 +303,7 @@ def test_srcset_signs_urls

signature_base = 'MYT0KEN' + '/image.jpg' + params;
expected_signature = Digest::MD5.hexdigest(signature_base)

assert_equal expected_signature, generated_signature
}
end
Expand Down Expand Up @@ -337,7 +337,6 @@ def test_srcset_within_bounds
# parse out the width descriptor as an integer
min = min.split(' ')[1].to_i
max = max[max.length - 1].split(' ')[1].to_i

assert_operator min, :>=, 100
assert_operator max, :<=, 8192
end
Expand Down Expand Up @@ -397,4 +396,75 @@ def srcset
@client ||= Imgix::Client.new(host: 'testing.imgix.net', secure_url_token: 'MYT0KEN', include_library_param: false).path('image.jpg').to_srcset(width_tolerance: 0.20)
end
end
end

class SrcsetCustomWidths < Imgix::Test
def test_srcset_generates_width_pairs
expected_number_of_pairs = 4
assert_equal expected_number_of_pairs, srcset.split(',').length
end

def test_srcset_pair_values
resolutions = [100, 500, 1000, 1800]
srclist = srcset.split(',').map { |srcset_split|
srcset_split.split(' ')[1].to_i
}

for i in 0..srclist.length - 1 do
assert_equal(srclist[i], resolutions[i])
end
end

def test_srcset_within_bounds
min, *max = srcset.split(',')

# parse out the width descriptor as an integer
min = min.split(' ')[1].to_i
max = max[max.length - 1].split(' ')[1].to_i

assert_operator min, :>=, @widths[0]
assert_operator max, :<=, @widths[-1]
end

def test_invalid_widths_input_emits_error
assert_raises(ArgumentError) {
Imgix::Client.new(host: 'testing.imgix.net')
.path('image.jpg')
.to_srcset(widths: 'abc')
}
end

def test_non_integer_array_emits_error
assert_raises(ArgumentError) {
Imgix::Client.new(host: 'testing.imgix.net')
.path('image.jpg')
.to_srcset(widths: [100, 200, false])
}
end

def test_with_param_after
srcset = Imgix::Client.new(host: 'testing.imgix.net', secure_url_token: 'MYT0KEN', include_library_param: false)
.path('image.jpg')
.to_srcset(widths: [100, 200, 300], h:1000, fit:"clip")
assert_includes(srcset, "h=")
assert(not(srcset.include? "widths="))
end

def test_with_param_before
srcset = Imgix::Client.new(host: 'testing.imgix.net', secure_url_token: 'MYT0KEN', include_library_param: false)
.path('image.jpg')
.to_srcset(h:1000, fit:"clip", widths: [100, 200, 300])
assert_includes(srcset, "h=")
assert(not(srcset.include? "widths="))
end

private
def srcset
@widths = [100, 500, 1000, 1800]
@client ||= Imgix::Client.new(
host: 'testing.imgix.net',
include_library_param: false)
.path('image.jpg')
.to_srcset(widths: @widths)
end
end
end