1// Copyright 2013 Google LLC. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package imageproxy 16 17import ( 18 "fmt" 19 "net/http" 20 "net/url" 21 "regexp" 22 "sort" 23 "strconv" 24 "strings" 25) 26 27const ( 28 optFit = "fit" 29 optFlipVertical = "fv" 30 optFlipHorizontal = "fh" 31 optFormatJPEG = "jpeg" 32 optFormatPNG = "png" 33 optFormatTIFF = "tiff" 34 optRotatePrefix = "r" 35 optQualityPrefix = "q" 36 optSignaturePrefix = "s" 37 optSizeDelimiter = "x" 38 optScaleUp = "scaleUp" 39 optCropX = "cx" 40 optCropY = "cy" 41 optCropWidth = "cw" 42 optCropHeight = "ch" 43 optSmartCrop = "sc" 44) 45 46// URLError reports a malformed URL error. 47type URLError struct { 48 Message string 49 URL *url.URL 50} 51 52func (e URLError) Error() string { 53 return fmt.Sprintf("malformed URL %q: %s", e.URL, e.Message) 54} 55 56// Options specifies transformations to be performed on the requested image. 57type Options struct { 58 // See ParseOptions for interpretation of Width and Height values 59 Width float64 60 Height float64 61 62 // If true, resize the image to fit in the specified dimensions. Image 63 // will not be cropped, and aspect ratio will be maintained. 64 Fit bool 65 66 // Rotate image the specified degrees counter-clockwise. Valid values 67 // are 90, 180, 270. 68 Rotate int 69 70 FlipVertical bool 71 FlipHorizontal bool 72 73 // Quality of output image 74 Quality int 75 76 // HMAC Signature for signed requests. 77 Signature string 78 79 // Allow image to scale beyond its original dimensions. This value 80 // will always be overwritten by the value of Proxy.ScaleUp. 81 ScaleUp bool 82 83 // Desired image format. Valid values are "jpeg", "png", "tiff". 84 Format string 85 86 // Crop rectangle params 87 CropX float64 88 CropY float64 89 CropWidth float64 90 CropHeight float64 91 92 // Automatically find good crop points based on image content. 93 SmartCrop bool 94} 95 96func (o Options) String() string { 97 opts := []string{fmt.Sprintf("%v%s%v", o.Width, optSizeDelimiter, o.Height)} 98 if o.Fit { 99 opts = append(opts, optFit) 100 } 101 if o.Rotate != 0 { 102 opts = append(opts, fmt.Sprintf("%s%d", optRotatePrefix, o.Rotate)) 103 } 104 if o.FlipVertical { 105 opts = append(opts, optFlipVertical) 106 } 107 if o.FlipHorizontal { 108 opts = append(opts, optFlipHorizontal) 109 } 110 if o.Quality != 0 { 111 opts = append(opts, fmt.Sprintf("%s%d", optQualityPrefix, o.Quality)) 112 } 113 if o.Signature != "" { 114 opts = append(opts, fmt.Sprintf("%s%s", optSignaturePrefix, o.Signature)) 115 } 116 if o.ScaleUp { 117 opts = append(opts, optScaleUp) 118 } 119 if o.Format != "" { 120 opts = append(opts, o.Format) 121 } 122 if o.CropX != 0 { 123 opts = append(opts, fmt.Sprintf("%s%v", optCropX, o.CropX)) 124 } 125 if o.CropY != 0 { 126 opts = append(opts, fmt.Sprintf("%s%v", optCropY, o.CropY)) 127 } 128 if o.CropWidth != 0 { 129 opts = append(opts, fmt.Sprintf("%s%v", optCropWidth, o.CropWidth)) 130 } 131 if o.CropHeight != 0 { 132 opts = append(opts, fmt.Sprintf("%s%v", optCropHeight, o.CropHeight)) 133 } 134 if o.SmartCrop { 135 opts = append(opts, optSmartCrop) 136 } 137 sort.Strings(opts) 138 return strings.Join(opts, ",") 139} 140 141// transform returns whether o includes transformation options. Some fields 142// are not transform related at all (like Signature), and others only apply in 143// the presence of other fields (like Fit). A non-empty Format value is 144// assumed to involve a transformation. 145func (o Options) transform() bool { 146 return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || o.CropX != 0 || o.CropY != 0 || o.CropWidth != 0 || o.CropHeight != 0 147} 148 149// ParseOptions parses str as a list of comma separated transformation options. 150// The options can be specified in in order, with duplicate options overwriting 151// previous values. 152// 153// Rectangle Crop 154// 155// There are four options controlling rectangle crop: 156// 157// cx{x} - X coordinate of top left rectangle corner (default: 0) 158// cy{y} - Y coordinate of top left rectangle corner (default: 0) 159// cw{width} - rectangle width (default: image width) 160// ch{height} - rectangle height (default: image height) 161// 162// For all options, integer values are interpreted as exact pixel values and 163// floats between 0 and 1 are interpreted as percentages of the original image 164// size. Negative values for cx and cy are measured from the right and bottom 165// edges of the image, respectively. 166// 167// If the crop width or height exceed the width or height of the image, the 168// crop width or height will be adjusted, preserving the specified cx and cy 169// values. Rectangular crop is applied before any other transformations. 170// 171// Smart Crop 172// 173// The "sc" option will perform a content-aware smart crop to fit the 174// requested image width and height dimensions (see Size and Cropping below). 175// The smart crop option will override any requested rectangular crop. 176// 177// Size and Cropping 178// 179// The size option takes the general form "{width}x{height}", where width and 180// height are numbers. Integer values greater than 1 are interpreted as exact 181// pixel values. Floats between 0 and 1 are interpreted as percentages of the 182// original image size. If either value is omitted or set to 0, it will be 183// automatically set to preserve the aspect ratio based on the other dimension. 184// If a single number is provided (with no "x" separator), it will be used for 185// both height and width. 186// 187// Depending on the size options specified, an image may be cropped to fit the 188// requested size. In all cases, the original aspect ratio of the image will be 189// preserved; imageproxy will never stretch the original image. 190// 191// When no explicit crop mode is specified, the following rules are followed: 192// 193// - If both width and height values are specified, the image will be scaled to 194// fill the space, cropping if necessary to fit the exact dimension. 195// 196// - If only one of the width or height values is specified, the image will be 197// resized to fit the specified dimension, scaling the other dimension as 198// needed to maintain the aspect ratio. 199// 200// If the "fit" option is specified together with a width and height value, the 201// image will be resized to fit within a containing box of the specified size. 202// As always, the original aspect ratio will be preserved. Specifying the "fit" 203// option with only one of either width or height does the same thing as if 204// "fit" had not been specified. 205// 206// Rotation and Flips 207// 208// The "r{degrees}" option will rotate the image the specified number of 209// degrees, counter-clockwise. Valid degrees values are 90, 180, and 270. 210// 211// The "fv" option will flip the image vertically. The "fh" option will flip 212// the image horizontally. Images are flipped after being rotated. 213// 214// Quality 215// 216// The "q{qualityPercentage}" option can be used to specify the quality of the 217// output file (JPEG only). If not specified, the default value of "95" is used. 218// 219// Format 220// 221// The "jpeg", "png", and "tiff" options can be used to specify the desired 222// image format of the proxied image. 223// 224// Signature 225// 226// The "s{signature}" option specifies an optional base64 encoded HMAC used to 227// sign the remote URL in the request. The HMAC key used to verify signatures is 228// provided to the imageproxy server on startup. 229// 230// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md 231// for examples of generating signatures. 232// 233// Examples 234// 235// 0x0 - no resizing 236// 200x - 200 pixels wide, proportional height 237// x0.15 - 15% original height, proportional width 238// 100x150 - 100 by 150 pixels, cropping as needed 239// 100 - 100 pixels square, cropping as needed 240// 150,fit - scale to fit 150 pixels square, no cropping 241// 100,r90 - 100 pixels square, rotated 90 degrees 242// 100,fv,fh - 100 pixels square, flipped horizontal and vertical 243// 200x,q60 - 200 pixels wide, proportional height, 60% quality 244// 200x,png - 200 pixels wide, converted to PNG format 245// cw100,ch100 - crop image to 100px square, starting at (0,0) 246// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall 247func ParseOptions(str string) Options { 248 var options Options 249 250 for _, opt := range strings.Split(str, ",") { 251 switch { 252 case len(opt) == 0: 253 break 254 case opt == optFit: 255 options.Fit = true 256 case opt == optFlipVertical: 257 options.FlipVertical = true 258 case opt == optFlipHorizontal: 259 options.FlipHorizontal = true 260 case opt == optScaleUp: // this option is intentionally not documented above 261 options.ScaleUp = true 262 case opt == optFormatJPEG, opt == optFormatPNG, opt == optFormatTIFF: 263 options.Format = opt 264 case opt == optSmartCrop: 265 options.SmartCrop = true 266 case strings.HasPrefix(opt, optRotatePrefix): 267 value := strings.TrimPrefix(opt, optRotatePrefix) 268 options.Rotate, _ = strconv.Atoi(value) 269 case strings.HasPrefix(opt, optQualityPrefix): 270 value := strings.TrimPrefix(opt, optQualityPrefix) 271 options.Quality, _ = strconv.Atoi(value) 272 case strings.HasPrefix(opt, optSignaturePrefix): 273 options.Signature = strings.TrimPrefix(opt, optSignaturePrefix) 274 case strings.HasPrefix(opt, optCropX): 275 value := strings.TrimPrefix(opt, optCropX) 276 options.CropX, _ = strconv.ParseFloat(value, 64) 277 case strings.HasPrefix(opt, optCropY): 278 value := strings.TrimPrefix(opt, optCropY) 279 options.CropY, _ = strconv.ParseFloat(value, 64) 280 case strings.HasPrefix(opt, optCropWidth): 281 value := strings.TrimPrefix(opt, optCropWidth) 282 options.CropWidth, _ = strconv.ParseFloat(value, 64) 283 case strings.HasPrefix(opt, optCropHeight): 284 value := strings.TrimPrefix(opt, optCropHeight) 285 options.CropHeight, _ = strconv.ParseFloat(value, 64) 286 case strings.Contains(opt, optSizeDelimiter): 287 size := strings.SplitN(opt, optSizeDelimiter, 2) 288 if w := size[0]; w != "" { 289 options.Width, _ = strconv.ParseFloat(w, 64) 290 } 291 if h := size[1]; h != "" { 292 options.Height, _ = strconv.ParseFloat(h, 64) 293 } 294 default: 295 if size, err := strconv.ParseFloat(opt, 64); err == nil { 296 options.Width = size 297 options.Height = size 298 } 299 } 300 } 301 302 return options 303} 304 305// Request is an imageproxy request which includes a remote URL of an image to 306// proxy, and an optional set of transformations to perform. 307type Request struct { 308 URL *url.URL // URL of the image to proxy 309 Options Options // Image transformation to perform 310 Original *http.Request // The original HTTP request 311} 312 313// String returns the request URL as a string, with r.Options encoded in the 314// URL fragment. 315func (r Request) String() string { 316 u := *r.URL 317 u.Fragment = r.Options.String() 318 return u.String() 319} 320 321// NewRequest parses an http.Request into an imageproxy Request. Options and 322// the remote image URL are specified in the request path, formatted as: 323// /{options}/{remote_url}. Options may be omitted, so a request path may 324// simply contain /{remote_url}. The remote URL must be an absolute "http" or 325// "https" URL, should not be URL encoded, and may contain a query string. 326// 327// Assuming an imageproxy server running on localhost, the following are all 328// valid imageproxy requests: 329// 330// http://localhost/100x200/http://example.com/image.jpg 331// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar 332// http://localhost//http://example.com/image.jpg 333// http://localhost/http://example.com/image.jpg 334func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { 335 var err error 336 req := &Request{Original: r} 337 338 path := r.URL.EscapedPath()[1:] // strip leading slash 339 req.URL, err = parseURL(path) 340 if err != nil || !req.URL.IsAbs() { 341 // first segment should be options 342 parts := strings.SplitN(path, "/", 2) 343 if len(parts) != 2 { 344 return nil, URLError{"too few path segments", r.URL} 345 } 346 347 var err error 348 req.URL, err = parseURL(parts[1]) 349 if err != nil { 350 return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL} 351 } 352 353 req.Options = ParseOptions(parts[0]) 354 } 355 356 if baseURL != nil { 357 req.URL = baseURL.ResolveReference(req.URL) 358 } 359 360 if !req.URL.IsAbs() { 361 return nil, URLError{"must provide absolute remote URL", r.URL} 362 } 363 364 if req.URL.Scheme != "http" && req.URL.Scheme != "https" { 365 return nil, URLError{"remote URL must have http or https scheme", r.URL} 366 } 367 368 // query string is always part of the remote URL 369 req.URL.RawQuery = r.URL.RawQuery 370 return req, nil 371} 372 373var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`) 374 375// parseURL parses s as a URL, handling URLs that have been munged by 376// path.Clean or a webserver that collapses multiple slashes. 377func parseURL(s string) (*url.URL, error) { 378 s = reCleanedURL.ReplaceAllString(s, "$1://$2") 379 return url.Parse(s) 380} 381