forked from imgix/imgix-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
srcset.go
254 lines (216 loc) · 7.89 KB
/
srcset.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package imgix
import (
"log"
"math"
"net/url"
"strconv"
"strings"
)
// defaultMinWidth is the default minimum width used within a
// srcset width-range. Widths can be below this value; this
// is just the default value used internally in the TargetWidths
// function.
const defaultMinWidth int = 100
// defaultMaxWidth is the default maximum width used within a
// srcset width-range. While width values can be above
// this value, they are typically less than or equal to
// this value. This is only a default value used internally in
// the TargetWidths function.
const defaultMaxWidth int = 8192
// defaultTolerance is the default width tolerance (percentage).
const defaultTolerance float64 = 0.08
// DefaultWidths is an array of image widths generated by
// calling TargetWidths(100, 8192, 0.08). These defaults are quite
// good, cover a wide range of widths, and are easy to start with.
var DefaultWidths = []int{
100, 116, 135, 156,
181, 210, 244, 283,
328, 380, 441, 512,
594, 689, 799, 927,
1075, 1247, 1446, 1678,
1946, 2257, 2619, 3038,
3524, 4087, 4741, 5500,
6380, 7401, 8192}
type SrcsetOpts struct {
minWidth int
maxWidth int
tolerance float64
variableQuality bool
}
type SrcsetOption func(opt *SrcsetOpts)
// CreateSrcset creates a srcset attribute string. Given a path, set of
// IxParam parameters, and a set of SrcsetOptions, this function infers
// which kind of srcset attribute to create.
//
// If the params contain a width parameter or both height and aspect
// ratio parameters, a fixed-width srcset attribute will be created.
// This fixed-width srcset attribute will be dpr-based and have variable
// quality enabled by default. Variable quality can be disabled by
// passing WithVariableQuality(false).
//
// Otherwise if no explicit width, height, or aspect ratio were found
// this function will create a fluid-width srcset attribute wherein
// each URL (or image candidate string) is described by a width in the
// specified width-range.
func (b *URLBuilder) CreateSrcset(
path string,
params []IxParam,
options ...SrcsetOption) string {
urlParams := url.Values{}
for _, fn := range params {
fn(&urlParams)
}
opts := SrcsetOpts{
minWidth: defaultMinWidth,
maxWidth: defaultMaxWidth,
tolerance: defaultTolerance,
variableQuality: true}
for _, fn := range options {
fn(&opts)
}
// Check params contains a width (w) or height (h) _and_ aspect ratio (ar);
hasWidth := urlParams.Get("w") != ""
hasHeight := urlParams.Get("h") != ""
hasAspectRatio := urlParams.Get("ar") != ""
// If params has either a width or _both_ height and aspect ratio,
// build a dpr-based srcset attribute.
if hasWidth || (hasHeight && hasAspectRatio) {
return b.buildSrcSetDpr(path, urlParams, opts.variableQuality)
}
// Otherwise, get the widthRange values from the opts and build a
// width-pairs based srcset attribute.
targets := TargetWidths(opts.minWidth, opts.maxWidth, opts.tolerance)
return b.buildSrcSetPairs(path, urlParams, targets)
}
func WithMinWidth(minWidth int) SrcsetOption {
return func(s *SrcsetOpts) {
s.minWidth = minWidth
}
}
func WithMaxWidth(maxWidth int) SrcsetOption {
return func(s *SrcsetOpts) {
s.maxWidth = maxWidth
}
}
func WithTolerance(tolerance float64) SrcsetOption {
return func(s *SrcsetOpts) {
s.tolerance = tolerance
}
}
func WithVariableQuality(variableQuality bool) SrcsetOption {
return func(s *SrcsetOpts) {
s.variableQuality = variableQuality
}
}
// CreateSrcsetFromWidths takes a path, a set of params, and an array of widths
// to create a srcset attribute with width-described URLs (image candidate strings).
func (b *URLBuilder) CreateSrcsetFromWidths(path string, params []IxParam, widths []int) string {
urlParams := url.Values{}
for _, fn := range params {
fn(&urlParams)
}
return b.buildSrcSetPairs(path, urlParams, widths)
}
// buildSrcSetPairs builds a srcset attribute string containing width-described
// image candidate strings.
func (b *URLBuilder) buildSrcSetPairs(path string, params url.Values, targets []int) string {
var srcSetEntries []string
for _, w := range targets {
widthValue := strconv.Itoa(w)
params.Set("w", widthValue)
entry := b.createImageCandidateString(path, params, widthValue+"w")
srcSetEntries = append(srcSetEntries, entry)
}
return strings.Join(srcSetEntries, ",\n")
}
func (b *URLBuilder) buildSrcSetDpr(path string, params url.Values, useVariableQuality bool) string {
var DprQualities = map[string]string{"1": "75", "2": "50", "3": "35", "4": "23", "5": "20"}
var srcSetEntries []string
qValue := params.Get("q")
// We could iterate over the map directly, but that doesn't yield
// deterministic results, ie. 5x might come before 1x in the final
// srcset attribute string. To prevent this, we iterate over the
// map "in order."
for i := 0; i < len(DprQualities); i++ {
ratio := strconv.Itoa(i + 1)
params.Set("dpr", ratio)
dprQuality := DprQualities[ratio]
if useVariableQuality && qValue != "" {
params.Set("q", qValue)
} else if useVariableQuality {
params.Set("q", dprQuality)
} else if qValue != "" {
params.Set("q", qValue)
}
entry := b.createImageCandidateString(path, params, ratio+"x")
srcSetEntries = append(srcSetEntries, entry)
}
return strings.Join(srcSetEntries, ",\n")
}
// createImageCandidateString joins a URL with a space and a suffix in order
// to create an image candidate string. For more information see:
// https://html.spec.whatwg.org/multipage/images.html#srcset-attributes
func (b *URLBuilder) createImageCandidateString(path string, params url.Values, suffix string) string {
return strings.Join([]string{b.createURLFromValues(path, params), " ", suffix}, "")
}
// TargetWidths creates an array of integer image widths.
// The image widths begin at the minWidth value and end at the
// maxWidth value––with a defaultTolerance amount of tolerable image
// width-variance between them.
func TargetWidths(minWidth int, maxWidth int, tolerance float64) []int {
validRange, err := validateRangeWithTolerance(minWidth, maxWidth, tolerance)
if err != nil {
log.Fatalln(err)
}
begin := validRange.minWidth
end := validRange.maxWidth
tol := validRange.tolerance
if isNotCustom(begin, end, tol) {
return DefaultWidths
}
if begin == end {
return []int{begin}
}
var resolutions []int
var start = float64(begin)
for int(start) < end && int(start) < defaultMaxWidth {
resolutions = append(resolutions, int(math.Round(start)))
start = start * (1.0 + tol*2.0)
}
lengthOfResolutions := len(resolutions)
// If we make it here, the lengthOfResolutions is greater
// than or equal to 2, so accessing the last element of
// the slice should not panic.
if resolutions != nil && resolutions[lengthOfResolutions-1] < end {
resolutions = append(resolutions, end)
}
return resolutions
}
// isDprBased determines if we can infer from params whether we need
// to create a dpr-based srcset attribute. If a width ("w") is present
// or if both the height ("h") and the aspect ratio ("ar") are present,
// then we can infer the desired srcset is dpr-based.
func (b *URLBuilder) isDprBased(params url.Values) bool {
const EmptyStr = ""
hasWidth := params.Get("w")
hasHeight := params.Get("h")
hasAspectRatio := params.Get("ar")
if hasWidth != EmptyStr {
return true
}
if hasHeight != EmptyStr && hasAspectRatio != EmptyStr {
return true
}
// Getting "w", "h", and "ar" returned empty strings so none are
// present in the params, this is _not_ a dpr-based srcset.
return false
}
// isNotCustom takes minWidth, maxWidth, and tolerance values and
// compares each to its respective default value. If every value is
// equal to its default value then this range isNotCustom, return true.
func isNotCustom(minWidth int, maxWidth int, tolerance float64) bool {
defaultMin := minWidth == defaultMinWidth
defaultMax := maxWidth == defaultMaxWidth
defaultTol := tolerance == defaultTolerance
return defaultMin && defaultMax && defaultTol
}