2021-03-10 12:24:13 -08:00
// Copyright 2013 The imageproxy authors.
// SPDX-License-Identifier: Apache-2.0
2013-12-06 17:40:35 -08:00
2014-07-30 18:23:43 -07:00
package imageproxy
2013-12-04 00:37:13 -08:00
import (
"fmt"
2013-12-26 14:38:15 -08:00
"net/http"
2013-12-04 00:37:13 -08:00
"net/url"
2016-05-26 13:22:17 -07:00
"regexp"
2019-03-27 17:20:16 +00:00
"sort"
2013-12-04 00:37:13 -08:00
"strconv"
"strings"
)
2015-01-25 00:30:15 -08:00
const (
2015-05-11 19:36:42 -07:00
optFit = "fit"
optFlipVertical = "fv"
optFlipHorizontal = "fh"
2017-06-01 07:51:14 -07:00
optFormatJPEG = "jpeg"
optFormatPNG = "png"
2017-08-31 17:03:17 -07:00
optFormatTIFF = "tiff"
2015-05-11 19:36:42 -07:00
optRotatePrefix = "r"
optQualityPrefix = "q"
optSignaturePrefix = "s"
optSizeDelimiter = "x"
2015-12-07 23:06:02 -08:00
optScaleUp = "scaleUp"
2017-05-15 19:55:49 +02:00
optCropX = "cx"
optCropY = "cy"
2017-08-31 03:05:09 +00:00
optCropWidth = "cw"
optCropHeight = "ch"
2017-09-27 00:54:15 +00:00
optSmartCrop = "sc"
2025-03-28 10:11:43 +01:00
optTrim = "trim"
2015-01-25 00:30:15 -08:00
)
2013-12-26 18:31:20 -08:00
// URLError reports a malformed URL error.
type URLError struct {
Message string
URL * url . URL
}
func ( e URLError ) Error ( ) string {
return fmt . Sprintf ( "malformed URL %q: %s" , e . URL , e . Message )
}
2014-11-23 15:34:39 -08:00
// Options specifies transformations to be performed on the requested image.
2013-12-04 23:12:44 -08:00
type Options struct {
2014-11-23 15:34:39 -08:00
// See ParseOptions for interpretation of Width and Height values
Width float64
Height float64
2013-12-06 11:01:34 -08:00
// If true, resize the image to fit in the specified dimensions. Image
// will not be cropped, and aspect ratio will be maintained.
Fit bool
2013-12-06 18:03:16 -08:00
2014-11-23 15:34:39 -08:00
// Rotate image the specified degrees counter-clockwise. Valid values
// are 90, 180, 270.
2013-12-06 18:03:16 -08:00
Rotate int
2013-12-06 22:18:44 -08:00
FlipVertical bool
FlipHorizontal bool
2015-01-12 17:32:00 +00:00
// Quality of output image
Quality int
2015-05-11 19:36:42 -07:00
// HMAC Signature for signed requests.
Signature string
2015-08-12 14:39:38 -04:00
2015-12-07 23:06:02 -08:00
// Allow image to scale beyond its original dimensions. This value
// will always be overwritten by the value of Proxy.ScaleUp.
2015-08-12 14:39:38 -04:00
ScaleUp bool
2017-06-01 07:51:14 -07:00
2017-08-31 17:03:17 -07:00
// Desired image format. Valid values are "jpeg", "png", "tiff".
2017-06-01 07:51:14 -07:00
Format string
2017-05-15 19:55:49 +02:00
// Crop rectangle params
2017-08-31 07:27:35 +00:00
CropX float64
CropY float64
CropWidth float64
CropHeight float64
2017-09-27 00:54:15 +00:00
// Automatically find good crop points based on image content.
SmartCrop bool
2025-03-28 10:11:43 +01:00
// If true, automatically trim pixels of the same color around the edges
Trim bool
2013-12-04 00:37:13 -08:00
}
2013-12-04 23:12:44 -08:00
func ( o Options ) String ( ) string {
2017-06-01 09:53:29 -07:00
opts := [ ] string { fmt . Sprintf ( "%v%s%v" , o . Width , optSizeDelimiter , o . Height ) }
2013-12-26 12:50:22 -08:00
if o . Fit {
2017-06-01 09:53:29 -07:00
opts = append ( opts , optFit )
2013-12-26 12:50:22 -08:00
}
if o . Rotate != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%d" , optRotatePrefix , o . Rotate ) )
2013-12-26 12:50:22 -08:00
}
if o . FlipVertical {
2017-06-01 09:53:29 -07:00
opts = append ( opts , optFlipVertical )
2013-12-26 12:50:22 -08:00
}
if o . FlipHorizontal {
2017-06-01 09:53:29 -07:00
opts = append ( opts , optFlipHorizontal )
2013-12-26 12:50:22 -08:00
}
2015-05-11 19:29:25 -07:00
if o . Quality != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%d" , optQualityPrefix , o . Quality ) )
2015-05-11 19:29:25 -07:00
}
2015-05-11 19:36:42 -07:00
if o . Signature != "" {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%s" , optSignaturePrefix , o . Signature ) )
2015-05-11 19:36:42 -07:00
}
2015-12-07 23:06:02 -08:00
if o . ScaleUp {
2017-06-01 09:53:29 -07:00
opts = append ( opts , optScaleUp )
2015-12-07 23:06:02 -08:00
}
2017-06-01 07:51:14 -07:00
if o . Format != "" {
2017-06-01 09:53:29 -07:00
opts = append ( opts , o . Format )
2017-06-01 07:51:14 -07:00
}
2017-05-15 19:55:49 +02:00
if o . CropX != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%v" , optCropX , o . CropX ) )
2017-05-15 19:55:49 +02:00
}
if o . CropY != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%v" , optCropY , o . CropY ) )
2017-05-15 19:55:49 +02:00
}
2017-08-31 03:05:09 +00:00
if o . CropWidth != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%v" , optCropWidth , o . CropWidth ) )
2017-08-31 03:05:09 +00:00
}
if o . CropHeight != 0 {
2019-10-13 19:28:22 -07:00
opts = append ( opts , fmt . Sprintf ( "%s%v" , optCropHeight , o . CropHeight ) )
2017-08-31 03:05:09 +00:00
}
2017-09-27 00:54:15 +00:00
if o . SmartCrop {
opts = append ( opts , optSmartCrop )
}
2025-03-28 10:11:43 +01:00
if o . Trim {
opts = append ( opts , optTrim )
}
2019-03-27 17:20:16 +00:00
sort . Strings ( opts )
2017-06-01 09:53:29 -07:00
return strings . Join ( opts , "," )
2013-12-04 00:37:13 -08:00
}
2016-05-02 19:23:00 -07:00
// transform returns whether o includes transformation options. Some fields
// are not transform related at all (like Signature), and others only apply in
2017-06-01 09:40:12 -07:00
// the presence of other fields (like Fit). A non-empty Format value is
// assumed to involve a transformation.
2016-05-02 19:23:00 -07:00
func ( o Options ) transform ( ) bool {
2025-03-28 10:11:43 +01:00
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 || o . Trim
2016-05-02 19:23:00 -07:00
}
2014-11-23 15:34:39 -08:00
// ParseOptions parses str as a list of comma separated transformation options.
2017-06-01 07:29:09 -07:00
// The options can be specified in in order, with duplicate options overwriting
// previous values.
2014-11-23 15:34:39 -08:00
//
2022-11-08 16:43:39 -08:00
// # Rectangle Crop
2017-05-15 19:55:49 +02:00
//
// There are four options controlling rectangle crop:
//
2022-11-08 16:43:39 -08:00
// cx{x} - X coordinate of top left rectangle corner (default: 0)
// cy{y} - Y coordinate of top left rectangle corner (default: 0)
// cw{width} - rectangle width (default: image width)
// ch{height} - rectangle height (default: image height)
2017-05-15 19:55:49 +02:00
//
2017-08-31 07:27:35 +00:00
// For all options, integer values are interpreted as exact pixel values and
// floats between 0 and 1 are interpreted as percentages of the original image
// size. Negative values for cx and cy are measured from the right and bottom
// edges of the image, respectively.
//
// If the crop width or height exceed the width or height of the image, the
// crop width or height will be adjusted, preserving the specified cx and cy
// values. Rectangular crop is applied before any other transformations.
2017-05-15 19:55:49 +02:00
//
2022-11-08 16:43:39 -08:00
// # Smart Crop
2017-09-27 00:54:15 +00:00
//
// The "sc" option will perform a content-aware smart crop to fit the
// requested image width and height dimensions (see Size and Cropping below).
// The smart crop option will override any requested rectangular crop.
//
2022-11-08 16:43:39 -08:00
// # Size and Cropping
2014-11-23 15:34:39 -08:00
//
// The size option takes the general form "{width}x{height}", where width and
// height are numbers. Integer values greater than 1 are interpreted as exact
// pixel values. Floats between 0 and 1 are interpreted as percentages of the
// original image size. If either value is omitted or set to 0, it will be
// automatically set to preserve the aspect ratio based on the other dimension.
// If a single number is provided (with no "x" separator), it will be used for
// both height and width.
//
// Depending on the size options specified, an image may be cropped to fit the
// requested size. In all cases, the original aspect ratio of the image will be
// preserved; imageproxy will never stretch the original image.
//
// When no explicit crop mode is specified, the following rules are followed:
//
// - If both width and height values are specified, the image will be scaled to
// fill the space, cropping if necessary to fit the exact dimension.
//
// - If only one of the width or height values is specified, the image will be
// resized to fit the specified dimension, scaling the other dimension as
// needed to maintain the aspect ratio.
//
// If the "fit" option is specified together with a width and height value, the
// image will be resized to fit within a containing box of the specified size.
// As always, the original aspect ratio will be preserved. Specifying the "fit"
// option with only one of either width or height does the same thing as if
// "fit" had not been specified.
//
2022-11-08 16:43:39 -08:00
// # Rotation and Flips
2014-11-23 15:34:39 -08:00
//
// The "r{degrees}" option will rotate the image the specified number of
// degrees, counter-clockwise. Valid degrees values are 90, 180, and 270.
//
// The "fv" option will flip the image vertically. The "fh" option will flip
// the image horizontally. Images are flipped after being rotated.
//
2022-11-08 16:43:39 -08:00
// # Quality
2015-01-12 17:32:00 +00:00
//
// The "q{qualityPercentage}" option can be used to specify the quality of the
2017-06-01 07:29:09 -07:00
// output file (JPEG only). If not specified, the default value of "95" is used.
//
2022-11-08 16:43:39 -08:00
// # Format
2017-06-01 07:51:14 -07:00
//
2025-03-28 10:11:43 +01:00
// The "jpeg", "png", and "tiff" options can be used to specify the desired
2017-08-31 17:03:17 -07:00
// image format of the proxied image.
2017-06-01 07:51:14 -07:00
//
2022-11-08 16:43:39 -08:00
// # Signature
2017-06-01 07:29:09 -07:00
//
// The "s{signature}" option specifies an optional base64 encoded HMAC used to
// sign the remote URL in the request. The HMAC key used to verify signatures is
// provided to the imageproxy server on startup.
//
2019-05-13 11:28:08 -04:00
// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md
2017-06-01 07:29:09 -07:00
// for examples of generating signatures.
2015-01-12 17:32:00 +00:00
//
2025-03-28 10:11:43 +01:00
// # Trim
//
// The "trim" option will automatically trim pixels of the same color around
// the edges of the image. This is useful for removing borders from images
// that have been resized or cropped. The trim option is applied before other
// options such as cropping or resizing.
//
2014-11-23 15:34:39 -08:00
// Examples
//
2022-11-08 16:43:39 -08:00
// 0x0 - no resizing
// 200x - 200 pixels wide, proportional height
// x0.15 - 15% original height, proportional width
// 100x150 - 100 by 150 pixels, cropping as needed
// 100 - 100 pixels square, cropping as needed
// 150,fit - scale to fit 150 pixels square, no cropping
// 100,r90 - 100 pixels square, rotated 90 degrees
// 100,fv,fh - 100 pixels square, flipped horizontal and vertical
// 200x,q60 - 200 pixels wide, proportional height, 60% quality
// 200x,png - 200 pixels wide, converted to PNG format
// cw100,ch100 - crop image to 100px square, starting at (0,0)
// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall
2014-11-19 21:59:52 -08:00
func ParseOptions ( str string ) Options {
2015-05-04 10:26:22 -07:00
var options Options
2014-11-23 15:34:39 -08:00
for _ , opt := range strings . Split ( str , "," ) {
switch {
2020-09-09 22:36:58 -07:00
case len ( opt ) == 0 : // do nothing
2015-01-25 00:30:15 -08:00
case opt == optFit :
2014-11-23 15:34:39 -08:00
options . Fit = true
2015-01-25 00:30:15 -08:00
case opt == optFlipVertical :
2014-11-23 15:34:39 -08:00
options . FlipVertical = true
2015-01-25 00:30:15 -08:00
case opt == optFlipHorizontal :
2014-11-23 15:34:39 -08:00
options . FlipHorizontal = true
2015-12-07 23:06:02 -08:00
case opt == optScaleUp : // this option is intentionally not documented above
options . ScaleUp = true
2017-08-31 17:03:17 -07:00
case opt == optFormatJPEG , opt == optFormatPNG , opt == optFormatTIFF :
2017-06-01 07:51:14 -07:00
options . Format = opt
2017-09-27 00:54:15 +00:00
case opt == optSmartCrop :
options . SmartCrop = true
2025-03-28 10:11:43 +01:00
case opt == optTrim :
options . Trim = true
2015-01-25 00:30:15 -08:00
case strings . HasPrefix ( opt , optRotatePrefix ) :
value := strings . TrimPrefix ( opt , optRotatePrefix )
options . Rotate , _ = strconv . Atoi ( value )
2015-01-12 17:32:00 +00:00
case strings . HasPrefix ( opt , optQualityPrefix ) :
value := strings . TrimPrefix ( opt , optQualityPrefix )
options . Quality , _ = strconv . Atoi ( value )
2015-05-11 19:36:42 -07:00
case strings . HasPrefix ( opt , optSignaturePrefix ) :
options . Signature = strings . TrimPrefix ( opt , optSignaturePrefix )
2017-05-15 19:55:49 +02:00
case strings . HasPrefix ( opt , optCropX ) :
value := strings . TrimPrefix ( opt , optCropX )
2017-08-31 07:27:35 +00:00
options . CropX , _ = strconv . ParseFloat ( value , 64 )
2017-05-15 19:55:49 +02:00
case strings . HasPrefix ( opt , optCropY ) :
value := strings . TrimPrefix ( opt , optCropY )
2017-08-31 07:27:35 +00:00
options . CropY , _ = strconv . ParseFloat ( value , 64 )
2017-08-31 03:05:09 +00:00
case strings . HasPrefix ( opt , optCropWidth ) :
value := strings . TrimPrefix ( opt , optCropWidth )
2017-08-31 07:27:35 +00:00
options . CropWidth , _ = strconv . ParseFloat ( value , 64 )
2017-08-31 03:05:09 +00:00
case strings . HasPrefix ( opt , optCropHeight ) :
value := strings . TrimPrefix ( opt , optCropHeight )
2017-08-31 07:27:35 +00:00
options . CropHeight , _ = strconv . ParseFloat ( value , 64 )
2015-01-25 00:30:15 -08:00
case strings . Contains ( opt , optSizeDelimiter ) :
size := strings . SplitN ( opt , optSizeDelimiter , 2 )
2014-11-23 15:34:39 -08:00
if w := size [ 0 ] ; w != "" {
options . Width , _ = strconv . ParseFloat ( w , 64 )
2013-12-26 13:35:23 -08:00
}
2014-11-23 15:34:39 -08:00
if h := size [ 1 ] ; h != "" {
options . Height , _ = strconv . ParseFloat ( h , 64 )
2013-12-26 13:35:23 -08:00
}
2014-11-23 15:34:39 -08:00
default :
if size , err := strconv . ParseFloat ( opt , 64 ) ; err == nil {
options . Width = size
options . Height = size
2013-12-26 13:35:23 -08:00
}
}
2013-12-04 00:37:13 -08:00
}
2014-11-23 15:34:39 -08:00
return options
2013-12-04 00:37:13 -08:00
}
2014-11-23 15:34:39 -08:00
// Request is an imageproxy request which includes a remote URL of an image to
// proxy, and an optional set of transformations to perform.
2013-12-04 00:37:13 -08:00
type Request struct {
2015-06-14 18:26:40 +10:00
URL * url . URL // URL of the image to proxy
Options Options // Image transformation to perform
Original * http . Request // The original HTTP request
2013-12-04 00:37:13 -08:00
}
2013-12-04 02:55:56 -08:00
2015-05-11 17:12:03 -07:00
// String returns the request URL as a string, with r.Options encoded in the
// URL fragment.
func ( r Request ) String ( ) string {
u := * r . URL
u . Fragment = r . Options . String ( )
return u . String ( )
}
2014-11-23 15:34:39 -08:00
// NewRequest parses an http.Request into an imageproxy Request. Options and
// the remote image URL are specified in the request path, formatted as:
// /{options}/{remote_url}. Options may be omitted, so a request path may
2017-05-15 19:55:49 +02:00
// simply contain /{remote_url}. The remote URL must be an absolute "http" or
2014-11-23 15:34:39 -08:00
// "https" URL, should not be URL encoded, and may contain a query string.
//
// Assuming an imageproxy server running on localhost, the following are all
// valid imageproxy requests:
//
2022-11-08 16:43:39 -08:00
// http://localhost/100x200/http://example.com/image.jpg
// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar
// http://localhost//http://example.com/image.jpg
// http://localhost/http://example.com/image.jpg
2015-02-19 21:34:22 -08:00
func NewRequest ( r * http . Request , baseURL * url . URL ) ( * Request , error ) {
2013-12-26 14:38:15 -08:00
var err error
2025-04-25 15:23:22 -04:00
var additionalQuery string
2015-06-14 18:26:40 +10:00
req := & Request { Original : r }
2013-12-26 14:38:15 -08:00
2025-04-28 11:48:58 -04:00
path := r . URL . EscapedPath ( ) [ 1 : ] // strip leading slash
2025-04-25 15:29:16 -04:00
decodedPath , err , additionalQuery := decodeURL ( path ) // Conditionally decode url path param if it is url encoded this enables us to process image urls with query params built in
if err == nil {
path = decodedPath
}
2016-05-26 13:45:59 -07:00
req . URL , err = parseURL ( path )
2013-12-26 14:38:15 -08:00
if err != nil || ! req . URL . IsAbs ( ) {
2014-11-23 15:34:39 -08:00
// first segment should be options
2013-12-26 14:38:15 -08:00
parts := strings . SplitN ( path , "/" , 2 )
if len ( parts ) != 2 {
return nil , URLError { "too few path segments" , r . URL }
}
2015-05-04 10:26:22 -07:00
var err error
2016-05-26 13:45:59 -07:00
req . URL , err = parseURL ( parts [ 1 ] )
2013-12-26 14:38:15 -08:00
if err != nil {
return nil , URLError { fmt . Sprintf ( "unable to parse remote URL: %v" , err ) , r . URL }
}
req . Options = ParseOptions ( parts [ 0 ] )
}
2015-02-19 21:34:22 -08:00
if baseURL != nil {
req . URL = baseURL . ResolveReference ( req . URL )
}
2013-12-26 14:38:15 -08:00
if ! req . URL . IsAbs ( ) {
return nil , URLError { "must provide absolute remote URL" , r . URL }
}
if req . URL . Scheme != "http" && req . URL . Scheme != "https" {
2014-11-21 17:56:59 -08:00
return nil , URLError { "remote URL must have http or https scheme" , r . URL }
2013-12-26 14:38:15 -08:00
}
// query string is always part of the remote URL
2025-04-25 15:23:22 -04:00
req . URL . RawQuery = combineQueries ( r . URL . RawQuery , additionalQuery )
2013-12-26 14:38:15 -08:00
return req , nil
}
2016-05-26 13:45:59 -07:00
var reCleanedURL = regexp . MustCompile ( ` ^(https?):/+([^/]) ` )
// parseURL parses s as a URL, handling URLs that have been munged by
// path.Clean or a webserver that collapses multiple slashes.
func parseURL ( s string ) ( * url . URL , error ) {
s = reCleanedURL . ReplaceAllString ( s , "$1://$2" )
return url . Parse ( s )
}
2020-12-22 10:32:34 -05:00
var reIsEncodedUrl = regexp . MustCompile ( ` ^https?% ` )
2025-04-25 15:23:22 -04:00
func decodeURL ( s string ) ( string , error , string ) {
var startsWithHttp = strings . HasPrefix ( s , "http" )
var prefix = ""
var u = s
if ! startsWithHttp && strings . Contains ( s , "http" ) {
var parts = strings . SplitN ( s , "http" , 2 )
u = "http" + parts [ 1 ]
prefix = parts [ 0 ]
}
var isUrlEncoded = reIsEncodedUrl . MatchString ( u )
2025-04-28 11:48:58 -04:00
2025-04-25 15:23:22 -04:00
if isUrlEncoded {
u , err := url . QueryUnescape ( u )
if err != nil {
return u , err , ""
}
var parsed , err2 = url . Parse ( u )
if err2 != nil {
return u , err2 , ""
}
return prefix + u , err2 , parsed . RawQuery
} else {
2025-04-28 11:48:58 -04:00
return s , nil , ""
2025-04-25 15:23:22 -04:00
}
}
func combineQueries ( a string , b string ) string {
if a == "" {
return b
} else if b == "" {
return a
} else {
return a + "&" + b
}
}