1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2024-11-24 08:12:38 +02:00
imgproxy/processing_options.go

1225 lines
27 KiB
Go
Raw Normal View History

2018-09-07 15:37:25 +02:00
package main
import (
2018-10-05 17:17:36 +02:00
"context"
2018-09-07 15:37:25 +02:00
"encoding/base64"
"errors"
"fmt"
"net/http"
2018-11-02 17:35:21 +02:00
"net/url"
"regexp"
2018-09-07 15:37:25 +02:00
"strconv"
"strings"
2019-10-10 16:35:17 +02:00
"sync"
2020-02-27 18:30:31 +02:00
"github.com/imgproxy/imgproxy/v2/structdiff"
2018-09-07 15:37:25 +02:00
)
2019-07-22 13:27:18 +02:00
type urlOption struct {
Name string
Args []string
}
type urlOptions []urlOption
2018-09-07 19:41:06 +02:00
type processingHeaders struct {
Accept string
Width string
ViewportWidth string
DPR string
}
type gravityType int
const (
gravityUnknown gravityType = iota
gravityCenter
gravityNorth
gravityEast
gravitySouth
gravityWest
2018-10-19 11:47:11 +02:00
gravityNorthWest
gravityNorthEast
gravitySouthWest
gravitySouthEast
gravitySmart
2018-09-11 13:23:28 +02:00
gravityFocusPoint
)
var gravityTypes = map[string]gravityType{
2018-10-19 11:47:11 +02:00
"ce": gravityCenter,
"no": gravityNorth,
"ea": gravityEast,
"so": gravitySouth,
"we": gravityWest,
"nowe": gravityNorthWest,
"noea": gravityNorthEast,
"sowe": gravitySouthWest,
"soea": gravitySouthEast,
"sm": gravitySmart,
"fp": gravityFocusPoint,
2018-09-11 13:23:28 +02:00
}
type resizeType int
const (
resizeFit resizeType = iota
resizeFill
resizeCrop
2019-06-24 14:50:17 +02:00
resizeAuto
)
2018-09-07 15:37:25 +02:00
var resizeTypes = map[string]resizeType{
"fit": resizeFit,
"fill": resizeFill,
"crop": resizeCrop,
2019-06-24 14:50:17 +02:00
"auto": resizeAuto,
2018-09-07 15:37:25 +02:00
}
type rgbColor struct{ R, G, B uint8 }
2018-10-02 14:20:23 +02:00
var hexColorRegex = regexp.MustCompile("^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
const (
hexColorLongFormat = "%02x%02x%02x"
hexColorShortFormat = "%1x%1x%1x"
)
2019-06-20 14:49:25 +02:00
type gravityOptions struct {
Type gravityType
X, Y float64
}
2019-12-25 12:50:02 +02:00
type extendOptions struct {
Enabled bool
Gravity gravityOptions
}
type cropOptions struct {
2021-01-18 17:14:15 +02:00
Width float64
Height float64
Gravity gravityOptions
}
type paddingOptions struct {
Enabled bool
Top int
Right int
Bottom int
Left int
}
2020-01-17 11:54:50 +02:00
type trimOptions struct {
Enabled bool
Threshold float64
Smart bool
Color rgbColor
EqualHor bool
EqualVer bool
2020-01-17 11:54:50 +02:00
}
2018-10-19 11:47:11 +02:00
type watermarkOptions struct {
Enabled bool
Opacity float64
Replicate bool
2019-12-25 12:50:02 +02:00
Gravity gravityOptions
2018-10-28 14:05:57 +02:00
Scale float64
2018-10-19 11:47:11 +02:00
}
2018-09-07 15:37:25 +02:00
type processingOptions struct {
2021-01-12 16:43:09 +02:00
ResizingType resizeType
Width int
Height int
Dpr float64
Gravity gravityOptions
Enlarge bool
Extend extendOptions
Crop cropOptions
Padding paddingOptions
Trim trimOptions
2021-01-13 16:51:19 +02:00
Rotate int
2021-01-12 16:43:09 +02:00
Format imageType
Quality int
MaxBytes int
Flatten bool
Background rgbColor
Blur float32
Sharpen float32
StripMetadata bool
StripColorProfile bool
AutoRotate bool
2018-10-19 11:47:11 +02:00
2018-10-29 10:54:30 +02:00
CacheBuster string
2018-10-19 11:47:11 +02:00
Watermark watermarkOptions
PreferWebP bool
EnforceWebP bool
PreferAvif bool
EnforceAvif bool
Filename string
2018-10-04 14:02:24 +02:00
UsedPresets []string
2018-09-07 15:37:25 +02:00
}
2018-10-05 17:17:36 +02:00
const (
imageURLCtxKey = ctxKey("imageUrl")
processingOptionsCtxKey = ctxKey("processingOptions")
2018-11-02 17:35:21 +02:00
urlTokenPlain = "plain"
2018-11-15 13:19:27 +02:00
maxClientHintDPR = 8
2018-11-20 14:53:44 +02:00
msgForbidden = "Forbidden"
msgInvalidURL = "Invalid URL"
msgInvalidSource = "Invalid Source"
2018-10-05 17:17:36 +02:00
)
2018-09-11 13:48:16 +02:00
func (gt gravityType) String() string {
for k, v := range gravityTypes {
if v == gt {
return k
}
}
return ""
}
func (gt gravityType) MarshalJSON() ([]byte, error) {
for k, v := range gravityTypes {
if v == gt {
return []byte(fmt.Sprintf("%q", k)), nil
}
}
return []byte("null"), nil
}
2018-09-11 13:48:16 +02:00
func (rt resizeType) String() string {
for k, v := range resizeTypes {
if v == rt {
return k
}
}
return ""
}
func (rt resizeType) MarshalJSON() ([]byte, error) {
for k, v := range resizeTypes {
if v == rt {
return []byte(fmt.Sprintf("%q", k)), nil
}
}
return []byte("null"), nil
}
2019-10-10 16:35:17 +02:00
var (
_newProcessingOptions processingOptions
newProcessingOptionsOnce sync.Once
)
2019-09-11 12:40:07 +02:00
func newProcessingOptions() *processingOptions {
2019-10-10 16:35:17 +02:00
newProcessingOptionsOnce.Do(func() {
_newProcessingOptions = processingOptions{
2021-01-12 16:43:09 +02:00
ResizingType: resizeFit,
Width: 0,
Height: 0,
Gravity: gravityOptions{Type: gravityCenter},
Enlarge: false,
Extend: extendOptions{Enabled: false, Gravity: gravityOptions{Type: gravityCenter}},
Padding: paddingOptions{Enabled: false},
Trim: trimOptions{Enabled: false, Threshold: 10, Smart: true},
2021-01-13 16:51:19 +02:00
Rotate: 0,
2021-01-13 13:58:36 +02:00
Quality: 0,
2021-01-12 16:43:09 +02:00
MaxBytes: 0,
Format: imageTypeUnknown,
Background: rgbColor{255, 255, 255},
Blur: 0,
Sharpen: 0,
Dpr: 1,
Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityOptions{Type: gravityCenter}},
StripMetadata: conf.StripMetadata,
StripColorProfile: conf.StripColorProfile,
AutoRotate: conf.AutoRotate,
2019-10-10 16:35:17 +02:00
}
})
po := _newProcessingOptions
po.UsedPresets = make([]string, 0, len(conf.Presets))
return &po
2019-09-11 12:40:07 +02:00
}
2021-01-13 13:58:36 +02:00
func (po *processingOptions) getQuality() int {
q := po.Quality
if q == 0 {
q = conf.FormatQuality[po.Format]
}
if q == 0 {
q = conf.Quality
}
return q
}
2018-11-02 17:35:21 +02:00
func (po *processingOptions) isPresetUsed(name string) bool {
for _, usedName := range po.UsedPresets {
if usedName == name {
return true
}
}
return false
}
func (po *processingOptions) presetUsed(name string) {
po.UsedPresets = append(po.UsedPresets, name)
}
2019-10-10 16:35:17 +02:00
func (po *processingOptions) Diff() structdiff.Entries {
return structdiff.Diff(newProcessingOptions(), po)
}
func (po *processingOptions) String() string {
2019-10-10 16:35:17 +02:00
return po.Diff().String()
}
func (po *processingOptions) MarshalJSON() ([]byte, error) {
return po.Diff().MarshalJSON()
}
func colorFromHex(hexcolor string) (rgbColor, error) {
c := rgbColor{}
if !hexColorRegex.MatchString(hexcolor) {
return c, fmt.Errorf("Invalid hex color: %s", hexcolor)
}
if len(hexcolor) == 3 {
fmt.Sscanf(hexcolor, hexColorShortFormat, &c.R, &c.G, &c.B)
c.R *= 17
c.G *= 17
c.B *= 17
} else {
fmt.Sscanf(hexcolor, hexColorLongFormat, &c.R, &c.G, &c.B)
}
return c, nil
}
2018-11-02 17:35:21 +02:00
func decodeBase64URL(parts []string) (string, string, error) {
var format string
2019-08-28 16:36:11 +02:00
encoded := strings.Join(parts, "")
urlParts := strings.Split(encoded, ".")
if len(urlParts[0]) == 0 {
return "", "", errors.New("Image URL is empty")
}
2018-11-02 17:35:21 +02:00
if len(urlParts) > 2 {
2019-08-28 16:36:11 +02:00
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
2018-10-04 14:02:24 +02:00
}
2018-11-02 17:35:21 +02:00
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
format = urlParts[1]
}
2019-01-14 12:09:14 +02:00
imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "="))
2018-11-02 17:35:21 +02:00
if err != nil {
2019-08-28 16:36:11 +02:00
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
2018-11-02 17:35:21 +02:00
}
fullURL := fmt.Sprintf("%s%s", conf.BaseURL, string(imageURL))
2018-11-02 17:35:21 +02:00
return fullURL, format, nil
2018-10-04 14:02:24 +02:00
}
2018-11-02 17:35:21 +02:00
func decodePlainURL(parts []string) (string, string, error) {
var format string
2018-09-07 15:37:25 +02:00
2019-08-28 16:36:11 +02:00
encoded := strings.Join(parts, "/")
urlParts := strings.Split(encoded, "@")
if len(urlParts[0]) == 0 {
return "", "", errors.New("Image URL is empty")
}
2018-09-07 15:37:25 +02:00
if len(urlParts) > 2 {
2019-08-28 16:36:11 +02:00
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
2018-09-07 15:37:25 +02:00
}
2018-11-02 17:35:21 +02:00
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
format = urlParts[1]
2018-09-07 15:37:25 +02:00
}
2019-08-28 16:36:11 +02:00
unescaped, err := url.PathUnescape(urlParts[0])
if err != nil {
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
}
fullURL := fmt.Sprintf("%s%s", conf.BaseURL, unescaped)
return fullURL, format, nil
2018-11-02 17:35:21 +02:00
}
func decodeURL(parts []string) (string, string, error) {
if len(parts) == 0 {
2019-08-28 16:36:11 +02:00
return "", "", errors.New("Image URL is empty")
2018-09-07 15:37:25 +02:00
}
2018-11-02 17:35:21 +02:00
if parts[0] == urlTokenPlain && len(parts) > 1 {
return decodePlainURL(parts[1:])
}
return decodeBase64URL(parts)
2018-09-07 15:37:25 +02:00
}
func parseDimension(d *int, name, arg string) error {
if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
*d = v
} else {
return fmt.Errorf("Invalid %s: %s", name, arg)
2018-09-07 15:37:25 +02:00
}
return nil
}
func parseBoolOption(str string) bool {
b, err := strconv.ParseBool(str)
if err != nil {
logWarning("`%s` is not a valid boolean value. Treated as false", str)
}
return b
}
2019-06-20 14:49:25 +02:00
func isGravityOffcetValid(gravity gravityType, offset float64) bool {
if gravity == gravityCenter {
return true
}
return offset >= 0 && (gravity != gravityFocusPoint || offset <= 1)
}
func parseGravity(g *gravityOptions, args []string) error {
2019-06-20 14:49:25 +02:00
nArgs := len(args)
if nArgs > 3 {
return fmt.Errorf("Invalid gravity arguments: %v", args)
}
if t, ok := gravityTypes[args[0]]; ok {
g.Type = t
2018-09-07 15:37:25 +02:00
} else {
return fmt.Errorf("Invalid gravity: %s", args[0])
}
2019-06-20 14:49:25 +02:00
if g.Type == gravitySmart && nArgs > 1 {
return fmt.Errorf("Invalid gravity arguments: %v", args)
} else if g.Type == gravityFocusPoint && nArgs != 3 {
return fmt.Errorf("Invalid gravity arguments: %v", args)
}
2019-06-20 14:49:25 +02:00
if nArgs > 1 {
if x, err := strconv.ParseFloat(args[1], 64); err == nil && isGravityOffcetValid(g.Type, x) {
g.X = x
} else {
return fmt.Errorf("Invalid gravity X: %s", args[1])
}
2019-06-20 14:49:25 +02:00
}
2019-06-20 14:49:25 +02:00
if nArgs > 2 {
if y, err := strconv.ParseFloat(args[2], 64); err == nil && isGravityOffcetValid(g.Type, y) {
g.Y = y
} else {
return fmt.Errorf("Invalid gravity Y: %s", args[2])
}
2018-09-07 15:37:25 +02:00
}
return nil
}
func applyWidthOption(po *processingOptions, args []string) error {
2018-09-07 15:37:25 +02:00
if len(args) > 1 {
return fmt.Errorf("Invalid width arguments: %v", args)
2018-09-07 15:37:25 +02:00
}
return parseDimension(&po.Width, "width", args[0])
}
func applyHeightOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid height arguments: %v", args)
2018-09-07 15:37:25 +02:00
}
return parseDimension(&po.Height, "height", args[0])
2018-09-07 15:37:25 +02:00
}
func applyEnlargeOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid enlarge arguments: %v", args)
}
po.Enlarge = parseBoolOption(args[0])
2018-09-07 15:37:25 +02:00
return nil
}
2019-02-21 17:55:20 +02:00
func applyExtendOption(po *processingOptions, args []string) error {
2019-12-25 12:50:02 +02:00
if len(args) > 4 {
2019-06-17 12:15:10 +02:00
return fmt.Errorf("Invalid extend arguments: %v", args)
2019-02-21 17:55:20 +02:00
}
2019-12-25 12:50:02 +02:00
po.Extend.Enabled = parseBoolOption(args[0])
if len(args) > 1 {
if err := parseGravity(&po.Extend.Gravity, args[1:]); err != nil {
return err
}
if po.Extend.Gravity.Type == gravitySmart {
return errors.New("extend doesn't support smart gravity")
}
}
2019-02-21 17:55:20 +02:00
return nil
}
2018-09-07 15:37:25 +02:00
func applySizeOption(po *processingOptions, args []string) (err error) {
2019-12-25 12:50:02 +02:00
if len(args) > 7 {
2018-09-07 15:37:25 +02:00
return fmt.Errorf("Invalid size arguments: %v", args)
}
2018-10-03 17:33:22 +02:00
if len(args) >= 1 && len(args[0]) > 0 {
2018-09-07 15:37:25 +02:00
if err = applyWidthOption(po, args[0:1]); err != nil {
return
}
}
2018-10-03 17:33:22 +02:00
if len(args) >= 2 && len(args[1]) > 0 {
2018-09-07 15:37:25 +02:00
if err = applyHeightOption(po, args[1:2]); err != nil {
return
}
}
2019-02-21 17:55:20 +02:00
if len(args) >= 3 && len(args[2]) > 0 {
2018-09-07 15:37:25 +02:00
if err = applyEnlargeOption(po, args[2:3]); err != nil {
return
}
}
2019-12-25 12:50:02 +02:00
if len(args) >= 4 && len(args[3]) > 0 {
if err = applyExtendOption(po, args[3:]); err != nil {
2019-02-21 17:55:20 +02:00
return
}
}
2018-09-07 15:37:25 +02:00
return nil
}
2018-10-03 17:34:13 +02:00
func applyResizingTypeOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid resizing type arguments: %v", args)
2018-09-07 15:37:25 +02:00
}
if r, ok := resizeTypes[args[0]]; ok {
2019-10-11 13:05:20 +02:00
po.ResizingType = r
2018-09-07 15:37:25 +02:00
} else {
return fmt.Errorf("Invalid resize type: %s", args[0])
}
2018-10-03 17:34:13 +02:00
return nil
}
func applyResizeOption(po *processingOptions, args []string) error {
2019-12-25 12:50:02 +02:00
if len(args) > 8 {
2018-10-03 17:34:13 +02:00
return fmt.Errorf("Invalid resize arguments: %v", args)
}
if len(args[0]) > 0 {
if err := applyResizingTypeOption(po, args[0:1]); err != nil {
return err
}
}
2018-09-07 15:37:25 +02:00
if len(args) > 1 {
if err := applySizeOption(po, args[1:]); err != nil {
return err
}
}
return nil
}
2018-11-15 13:19:27 +02:00
func applyDprOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid dpr arguments: %v", args)
}
2019-06-19 12:14:41 +02:00
if d, err := strconv.ParseFloat(args[0], 64); err == nil && d > 0 {
2018-11-15 13:19:27 +02:00
po.Dpr = d
} else {
return fmt.Errorf("Invalid dpr: %s", args[0])
}
return nil
}
2018-09-07 15:37:25 +02:00
func applyGravityOption(po *processingOptions, args []string) error {
return parseGravity(&po.Gravity, args)
}
func applyCropOption(po *processingOptions, args []string) error {
if len(args) > 5 {
return fmt.Errorf("Invalid crop arguments: %v", args)
2018-09-07 15:37:25 +02:00
}
2021-01-18 17:14:15 +02:00
if w, err := strconv.ParseFloat(args[0], 64); err == nil && w >= 0 {
po.Crop.Width = w
} else {
return fmt.Errorf("Invalid crop width: %s", args[0])
}
2018-09-11 13:23:28 +02:00
if len(args) > 1 {
2021-01-18 17:14:15 +02:00
if h, err := strconv.ParseFloat(args[1], 64); err == nil && h >= 0 {
po.Crop.Height = h
} else {
return fmt.Errorf("Invalid crop height: %s", args[1])
2018-09-11 13:23:28 +02:00
}
}
2018-09-11 13:23:28 +02:00
if len(args) > 2 {
return parseGravity(&po.Crop.Gravity, args[2:])
2018-09-11 13:23:28 +02:00
}
2018-09-07 15:37:25 +02:00
return nil
}
func applyPaddingOption(po *processingOptions, args []string) error {
nArgs := len(args)
if nArgs < 1 || nArgs > 4 {
return fmt.Errorf("Invalid padding arguments: %v", args)
}
po.Padding.Enabled = true
if nArgs > 0 && len(args[0]) > 0 {
if err := parseDimension(&po.Padding.Top, "padding top (+all)", args[0]); err != nil {
return err
}
po.Padding.Right = po.Padding.Top
po.Padding.Bottom = po.Padding.Top
po.Padding.Left = po.Padding.Top
}
if nArgs > 1 && len(args[1]) > 0 {
if err := parseDimension(&po.Padding.Right, "padding right (+left)", args[1]); err != nil {
return err
}
po.Padding.Left = po.Padding.Right
}
if nArgs > 2 && len(args[2]) > 0 {
if err := parseDimension(&po.Padding.Bottom, "padding bottom", args[2]); err != nil {
return err
}
}
if nArgs > 3 && len(args[3]) > 0 {
if err := parseDimension(&po.Padding.Left, "padding left", args[3]); err != nil {
return err
}
}
if po.Padding.Top == 0 && po.Padding.Right == 0 && po.Padding.Bottom == 0 && po.Padding.Left == 0 {
po.Padding.Enabled = false
}
return nil
}
2020-01-17 11:54:50 +02:00
func applyTrimOption(po *processingOptions, args []string) error {
nArgs := len(args)
if nArgs > 4 {
return fmt.Errorf("Invalid trim arguments: %v", args)
2020-01-17 11:54:50 +02:00
}
if t, err := strconv.ParseFloat(args[0], 64); err == nil && t >= 0 {
po.Trim.Enabled = true
po.Trim.Threshold = t
} else {
return fmt.Errorf("Invalid trim threshold: %s", args[0])
}
if nArgs > 1 && len(args[1]) > 0 {
if c, err := colorFromHex(args[1]); err == nil {
po.Trim.Color = c
po.Trim.Smart = false
} else {
return fmt.Errorf("Invalid trim color: %s", args[1])
}
}
if nArgs > 2 && len(args[2]) > 0 {
po.Trim.EqualHor = parseBoolOption(args[2])
}
if nArgs > 3 && len(args[3]) > 0 {
po.Trim.EqualVer = parseBoolOption(args[3])
2020-01-17 11:54:50 +02:00
}
return nil
}
2021-01-13 16:51:19 +02:00
func applyRotateOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid rotate arguments: %v", args)
}
if r, err := strconv.Atoi(args[0]); err == nil && r%90 == 0 {
po.Rotate = r
} else {
return fmt.Errorf("Invalid rotation angle: %s", args[0])
}
return nil
}
2018-10-30 14:20:02 +02:00
func applyQualityOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid quality arguments: %v", args)
}
2021-01-13 13:58:36 +02:00
if q, err := strconv.Atoi(args[0]); err == nil && q >= 0 && q <= 100 {
2018-10-30 14:20:02 +02:00
po.Quality = q
} else {
return fmt.Errorf("Invalid quality: %s", args[0])
}
return nil
}
func applyMaxBytesOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid max_bytes arguments: %v", args)
}
if max, err := strconv.Atoi(args[0]); err == nil && max >= 0 {
po.MaxBytes = max
} else {
return fmt.Errorf("Invalid max_bytes: %s", args[0])
}
return nil
}
2018-10-02 14:20:23 +02:00
func applyBackgroundOption(po *processingOptions, args []string) error {
switch len(args) {
case 1:
if len(args[0]) == 0 {
po.Flatten = false
} else if c, err := colorFromHex(args[0]); err == nil {
po.Flatten = true
po.Background = c
} else {
return fmt.Errorf("Invalid background argument: %s", err)
}
2018-10-02 14:20:23 +02:00
case 3:
po.Flatten = true
2018-10-02 14:20:23 +02:00
if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r <= 255 {
po.Background.R = uint8(r)
} else {
return fmt.Errorf("Invalid background red channel: %s", args[0])
}
2018-10-02 14:20:23 +02:00
if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g <= 255 {
po.Background.G = uint8(g)
} else {
return fmt.Errorf("Invalid background green channel: %s", args[1])
}
2018-10-02 14:20:23 +02:00
if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b <= 255 {
po.Background.B = uint8(b)
} else {
return fmt.Errorf("Invalid background blue channel: %s", args[2])
}
default:
return fmt.Errorf("Invalid background arguments: %v", args)
}
2018-10-02 14:20:23 +02:00
return nil
}
2018-09-07 16:46:16 +02:00
func applyBlurOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid blur arguments: %v", args)
}
2019-05-28 13:39:20 +02:00
if b, err := strconv.ParseFloat(args[0], 32); err == nil && b >= 0 {
2018-09-07 16:46:16 +02:00
po.Blur = float32(b)
} else {
return fmt.Errorf("Invalid blur: %s", args[0])
}
return nil
}
func applySharpenOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid sharpen arguments: %v", args)
}
2019-05-28 13:39:20 +02:00
if s, err := strconv.ParseFloat(args[0], 32); err == nil && s >= 0 {
2018-09-07 16:46:16 +02:00
po.Sharpen = float32(s)
} else {
return fmt.Errorf("Invalid sharpen: %s", args[0])
}
return nil
}
2018-09-07 19:41:06 +02:00
func applyPresetOption(po *processingOptions, args []string) error {
for _, preset := range args {
if p, ok := conf.Presets[preset]; ok {
2018-10-04 14:02:24 +02:00
if po.isPresetUsed(preset) {
logWarning("Recursive preset usage is detected: %s", preset)
continue
2018-10-04 14:02:24 +02:00
}
po.presetUsed(preset)
if err := applyProcessingOptions(po, p); err != nil {
return err
2018-09-07 19:41:06 +02:00
}
} else {
2019-08-28 15:01:02 +02:00
return fmt.Errorf("Unknown preset: %s", preset)
2018-09-07 19:41:06 +02:00
}
}
return nil
}
2018-10-19 11:47:11 +02:00
func applyWatermarkOption(po *processingOptions, args []string) error {
2018-10-28 14:05:57 +02:00
if len(args) > 7 {
return fmt.Errorf("Invalid watermark arguments: %v", args)
}
2018-10-19 11:47:11 +02:00
if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
po.Watermark.Enabled = o > 0
po.Watermark.Opacity = o
} else {
return fmt.Errorf("Invalid watermark opacity: %s", args[0])
}
2018-10-28 14:05:57 +02:00
if len(args) > 1 && len(args[1]) > 0 {
2018-10-19 11:47:11 +02:00
if args[1] == "re" {
po.Watermark.Replicate = true
2018-10-19 13:31:09 +02:00
} else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint && g != gravitySmart {
2019-12-25 12:50:02 +02:00
po.Watermark.Gravity.Type = g
2018-10-19 11:47:11 +02:00
} else {
return fmt.Errorf("Invalid watermark position: %s", args[1])
}
}
2018-10-28 14:05:57 +02:00
if len(args) > 2 && len(args[2]) > 0 {
2018-10-19 11:47:11 +02:00
if x, err := strconv.Atoi(args[2]); err == nil {
2019-12-25 12:50:02 +02:00
po.Watermark.Gravity.X = float64(x)
2018-10-19 11:47:11 +02:00
} else {
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
}
}
2018-10-28 14:05:57 +02:00
if len(args) > 3 && len(args[3]) > 0 {
2018-10-19 11:47:11 +02:00
if y, err := strconv.Atoi(args[3]); err == nil {
2019-12-25 12:50:02 +02:00
po.Watermark.Gravity.Y = float64(y)
2018-10-19 11:47:11 +02:00
} else {
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
}
}
2018-10-28 14:05:57 +02:00
if len(args) > 4 && len(args[4]) > 0 {
if s, err := strconv.ParseFloat(args[4], 64); err == nil && s >= 0 {
po.Watermark.Scale = s
} else {
return fmt.Errorf("Invalid watermark scale: %s", args[4])
}
}
2018-10-19 11:47:11 +02:00
return nil
}
2018-09-11 14:26:27 +02:00
func applyFormatOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid format arguments: %v", args)
}
if f, ok := imageTypes[args[0]]; ok {
po.Format = f
} else {
return fmt.Errorf("Invalid image format: %s", args[0])
}
2019-10-01 14:08:30 +02:00
if !imageTypeSaveSupport(po.Format) {
2019-08-28 16:36:11 +02:00
return fmt.Errorf("Resulting image format is not supported: %s", po.Format)
2018-09-11 14:26:27 +02:00
}
2018-09-07 15:37:25 +02:00
return nil
}
2018-10-29 10:54:30 +02:00
func applyCacheBusterOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid cache buster arguments: %v", args)
}
po.CacheBuster = args[0]
return nil
}
func applyFilenameOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid filename arguments: %v", args)
}
po.Filename = args[0]
return nil
}
func applyStripMetadataOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid strip metadata arguments: %v", args)
}
po.StripMetadata = parseBoolOption(args[0])
return nil
}
2021-01-12 16:43:09 +02:00
func applyStripColorProfileOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid strip color profile arguments: %v", args)
}
po.StripColorProfile = parseBoolOption(args[0])
return nil
}
func applyAutoRotateOption(po *processingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid auto rotate arguments: %v", args)
}
po.AutoRotate = parseBoolOption(args[0])
return nil
}
2018-09-07 15:37:25 +02:00
func applyProcessingOption(po *processingOptions, name string, args []string) error {
switch name {
2018-10-03 17:36:52 +02:00
case "format", "f", "ext":
2019-08-13 13:56:14 +02:00
return applyFormatOption(po, args)
2018-10-03 17:36:52 +02:00
case "resize", "rs":
2019-08-13 13:56:14 +02:00
return applyResizeOption(po, args)
2018-10-03 17:36:52 +02:00
case "resizing_type", "rt":
2019-08-13 13:56:14 +02:00
return applyResizingTypeOption(po, args)
2018-10-03 17:36:52 +02:00
case "size", "s":
2019-08-13 13:56:14 +02:00
return applySizeOption(po, args)
2018-10-03 17:36:52 +02:00
case "width", "w":
2019-08-13 13:56:14 +02:00
return applyWidthOption(po, args)
2018-10-03 17:36:52 +02:00
case "height", "h":
2019-08-13 13:56:14 +02:00
return applyHeightOption(po, args)
2018-10-03 17:36:52 +02:00
case "enlarge", "el":
2019-08-13 13:56:14 +02:00
return applyEnlargeOption(po, args)
2019-02-21 17:55:20 +02:00
case "extend", "ex":
2019-08-13 13:56:14 +02:00
return applyExtendOption(po, args)
2018-11-15 13:19:27 +02:00
case "dpr":
2019-08-13 13:56:14 +02:00
return applyDprOption(po, args)
2018-10-03 17:36:52 +02:00
case "gravity", "g":
2019-08-13 13:56:14 +02:00
return applyGravityOption(po, args)
case "crop", "c":
2019-08-13 13:56:14 +02:00
return applyCropOption(po, args)
2020-01-17 11:54:50 +02:00
case "trim", "t":
return applyTrimOption(po, args)
2021-01-13 16:51:19 +02:00
case "rotate", "rot":
return applyRotateOption(po, args)
case "padding", "pd":
return applyPaddingOption(po, args)
2018-10-30 14:20:02 +02:00
case "quality", "q":
2019-08-13 13:56:14 +02:00
return applyQualityOption(po, args)
case "max_bytes", "mb":
return applyMaxBytesOption(po, args)
2018-10-03 17:36:52 +02:00
case "background", "bg":
2019-08-13 13:56:14 +02:00
return applyBackgroundOption(po, args)
2018-10-03 17:36:52 +02:00
case "blur", "bl":
2019-08-13 13:56:14 +02:00
return applyBlurOption(po, args)
2018-10-03 17:36:52 +02:00
case "sharpen", "sh":
2019-08-13 13:56:14 +02:00
return applySharpenOption(po, args)
2018-10-19 11:47:11 +02:00
case "watermark", "wm":
2019-08-13 13:56:14 +02:00
return applyWatermarkOption(po, args)
2018-10-03 17:36:52 +02:00
case "preset", "pr":
2019-08-13 13:56:14 +02:00
return applyPresetOption(po, args)
2018-10-29 10:54:30 +02:00
case "cachebuster", "cb":
2019-08-13 13:56:14 +02:00
return applyCacheBusterOption(po, args)
case "strip_metadata", "sm":
return applyStripMetadataOption(po, args)
2021-01-12 16:43:09 +02:00
case "strip_color_profile", "scp":
return applyStripColorProfileOption(po, args)
case "auto_rotate", "ar":
return applyAutoRotateOption(po, args)
case "filename", "fn":
2019-08-13 13:56:14 +02:00
return applyFilenameOption(po, args)
}
2019-08-13 13:56:14 +02:00
return fmt.Errorf("Unknown processing option: %s", name)
}
func applyProcessingOptions(po *processingOptions, options urlOptions) error {
2019-07-22 13:27:18 +02:00
for _, opt := range options {
if err := applyProcessingOption(po, opt.Name, opt.Args); err != nil {
return err
}
2018-09-07 15:37:25 +02:00
}
return nil
}
func isAllowedSource(imageURL string) bool {
if len(conf.AllowedSources) == 0 {
return true
}
for _, val := range conf.AllowedSources {
if strings.HasPrefix(imageURL, string(val)) {
return true
}
}
return false
}
2018-09-07 19:41:06 +02:00
func parseURLOptions(opts []string) (urlOptions, []string) {
2019-07-22 13:27:18 +02:00
parsed := make(urlOptions, 0, len(opts))
2018-09-07 19:41:06 +02:00
urlStart := len(opts) + 1
2018-09-07 15:37:25 +02:00
2018-09-07 19:41:06 +02:00
for i, opt := range opts {
args := strings.Split(opt, ":")
2018-09-07 15:37:25 +02:00
if len(args) == 1 {
urlStart = i
break
}
2019-07-22 13:27:18 +02:00
parsed = append(parsed, urlOption{Name: args[0], Args: args[1:]})
2018-09-07 19:41:06 +02:00
}
var rest []string
if urlStart < len(opts) {
rest = opts[urlStart:]
} else {
rest = []string{}
}
return parsed, rest
}
func defaultProcessingOptions(headers *processingHeaders) (*processingOptions, error) {
2019-09-11 12:40:07 +02:00
po := newProcessingOptions()
2018-09-11 12:33:36 +02:00
if strings.Contains(headers.Accept, "image/webp") {
po.PreferWebP = conf.EnableWebpDetection || conf.EnforceWebp
po.EnforceWebP = conf.EnforceWebp
2018-09-11 14:26:27 +02:00
}
if strings.Contains(headers.Accept, "image/avif") {
po.PreferAvif = conf.EnableAvifDetection || conf.EnforceAvif
po.EnforceAvif = conf.EnforceAvif
}
if conf.EnableClientHints && len(headers.ViewportWidth) > 0 {
if vw, err := strconv.Atoi(headers.ViewportWidth); err == nil {
po.Width = vw
}
}
if conf.EnableClientHints && len(headers.Width) > 0 {
if w, err := strconv.Atoi(headers.Width); err == nil {
po.Width = w
}
}
if conf.EnableClientHints && len(headers.DPR) > 0 {
2019-05-28 13:39:20 +02:00
if dpr, err := strconv.ParseFloat(headers.DPR, 64); err == nil && (dpr > 0 && dpr <= maxClientHintDPR) {
2018-11-15 13:19:27 +02:00
po.Dpr = dpr
}
}
2018-09-11 12:33:36 +02:00
if _, ok := conf.Presets["default"]; ok {
2019-09-11 12:40:07 +02:00
if err := applyPresetOption(po, []string{"default"}); err != nil {
return po, err
}
2018-09-11 12:33:36 +02:00
}
2019-09-11 12:40:07 +02:00
return po, nil
2018-09-11 12:33:36 +02:00
}
func parsePathAdvanced(parts []string, headers *processingHeaders) (string, *processingOptions, error) {
po, err := defaultProcessingOptions(headers)
2018-09-11 12:33:36 +02:00
if err != nil {
return "", po, err
}
2018-09-07 19:41:06 +02:00
options, urlParts := parseURLOptions(parts)
if err = applyProcessingOptions(po, options); err != nil {
return "", po, err
2018-09-07 15:37:25 +02:00
}
2018-09-11 14:26:27 +02:00
url, extension, err := decodeURL(urlParts)
2018-09-07 15:37:25 +02:00
if err != nil {
return "", po, err
}
2018-09-11 14:26:27 +02:00
if len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
2018-10-05 17:17:36 +02:00
return "", po, err
2018-09-11 14:26:27 +02:00
}
2018-09-07 15:37:25 +02:00
}
2018-11-02 17:35:21 +02:00
return url, po, nil
2018-09-07 15:37:25 +02:00
}
func parsePathPresets(parts []string, headers *processingHeaders) (string, *processingOptions, error) {
po, err := defaultProcessingOptions(headers)
if err != nil {
return "", po, err
}
presets := strings.Split(parts[0], ":")
urlParts := parts[1:]
if err = applyPresetOption(po, presets); err != nil {
return "", nil, err
}
url, extension, err := decodeURL(urlParts)
if err != nil {
return "", po, err
}
if len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return "", po, err
}
}
return url, po, nil
}
2018-11-06 13:19:34 +02:00
func parsePathBasic(parts []string, headers *processingHeaders) (string, *processingOptions, error) {
2018-09-07 15:37:25 +02:00
if len(parts) < 6 {
2019-08-28 16:36:11 +02:00
return "", nil, fmt.Errorf("Invalid basic URL format arguments: %s", strings.Join(parts, "/"))
2018-09-11 12:33:36 +02:00
}
po, err := defaultProcessingOptions(headers)
2018-09-11 12:33:36 +02:00
if err != nil {
return "", po, err
2018-09-07 15:37:25 +02:00
}
2019-10-11 13:05:20 +02:00
po.ResizingType = resizeTypes[parts[0]]
2018-09-07 15:37:25 +02:00
2018-10-09 15:03:10 +02:00
if err = applyWidthOption(po, parts[1:2]); err != nil {
2018-09-07 15:37:25 +02:00
return "", po, err
}
2018-10-09 15:03:10 +02:00
if err = applyHeightOption(po, parts[2:3]); err != nil {
2018-09-07 15:37:25 +02:00
return "", po, err
}
2018-10-09 15:03:10 +02:00
if err = applyGravityOption(po, strings.Split(parts[3], ":")); err != nil {
2018-09-07 15:37:25 +02:00
return "", po, err
}
2018-10-09 15:03:10 +02:00
if err = applyEnlargeOption(po, parts[4:5]); err != nil {
2018-09-07 15:37:25 +02:00
return "", po, err
}
2018-09-11 14:26:27 +02:00
url, extension, err := decodeURL(parts[5:])
2018-09-07 15:37:25 +02:00
if err != nil {
return "", po, err
}
2018-09-11 14:26:27 +02:00
if len(extension) > 0 {
2018-10-09 15:03:10 +02:00
if err := applyFormatOption(po, []string{extension}); err != nil {
2018-10-05 17:17:36 +02:00
return "", po, err
2018-09-11 14:26:27 +02:00
}
2018-09-07 15:37:25 +02:00
}
2018-11-02 17:35:21 +02:00
return url, po, nil
2018-09-07 15:37:25 +02:00
}
func parsePath(ctx context.Context, r *http.Request) (context.Context, error) {
2020-04-07 13:54:00 +02:00
var err error
path := trimAfter(r.RequestURI, '?')
2020-04-07 13:54:00 +02:00
if len(conf.PathPrefix) > 0 {
path = strings.TrimPrefix(path, conf.PathPrefix)
}
path = strings.TrimPrefix(path, "/")
parts := strings.Split(path, "/")
2018-09-07 15:37:25 +02:00
if len(parts) < 2 {
2019-08-28 16:36:11 +02:00
return ctx, newError(404, fmt.Sprintf("Invalid path: %s", path), msgInvalidURL)
2018-09-07 15:37:25 +02:00
}
if !conf.AllowInsecure {
if err = validatePath(parts[0], strings.TrimPrefix(path, parts[0])); err != nil {
2018-11-20 14:53:44 +02:00
return ctx, newError(403, err.Error(), msgForbidden)
}
}
2018-11-02 18:39:38 +02:00
headers := &processingHeaders{
Accept: r.Header.Get("Accept"),
Width: r.Header.Get("Width"),
ViewportWidth: r.Header.Get("Viewport-Width"),
DPR: r.Header.Get("DPR"),
2018-11-02 18:39:38 +02:00
}
2018-09-07 15:37:25 +02:00
2018-10-05 17:17:36 +02:00
var imageURL string
2018-10-09 15:03:10 +02:00
var po *processingOptions
2018-10-05 17:17:36 +02:00
if conf.OnlyPresets {
imageURL, po, err = parsePathPresets(parts[1:], headers)
} else if _, ok := resizeTypes[parts[1]]; ok {
2018-11-06 13:19:34 +02:00
imageURL, po, err = parsePathBasic(parts[1:], headers)
2018-10-05 17:17:36 +02:00
} else {
imageURL, po, err = parsePathAdvanced(parts[1:], headers)
2018-10-05 17:17:36 +02:00
}
if err != nil {
2018-11-20 14:53:44 +02:00
return ctx, newError(404, err.Error(), msgInvalidURL)
}
if !isAllowedSource(imageURL) {
2020-07-31 16:07:39 +02:00
return ctx, newError(404, "Invalid source", msgInvalidSource)
}
2018-10-05 17:17:36 +02:00
ctx = context.WithValue(ctx, imageURLCtxKey, imageURL)
2018-10-09 15:03:10 +02:00
ctx = context.WithValue(ctx, processingOptionsCtxKey, po)
2018-10-05 17:17:36 +02:00
2018-11-20 14:53:44 +02:00
return ctx, nil
2018-10-05 17:17:36 +02:00
}
func getImageURL(ctx context.Context) string {
str, _ := ctx.Value(imageURLCtxKey).(string)
return str
2018-10-05 17:17:36 +02:00
}
2018-10-05 18:20:29 +02:00
func getProcessingOptions(ctx context.Context) *processingOptions {
2018-10-05 17:17:36 +02:00
return ctx.Value(processingOptionsCtxKey).(*processingOptions)
2018-09-07 15:37:25 +02:00
}