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