mirror of
https://github.com/imgproxy/imgproxy.git
synced 2024-11-24 08:12:38 +02:00
3d13e6afb2
Month of testing and profiling show that fasthttp doesn't gives us significatnt profit in memory and performance while being incompatible with many third-side packages and http/2
901 lines
19 KiB
Go
901 lines
19 KiB
Go
package main
|
|
|
|
/*
|
|
#cgo LDFLAGS: -s -w
|
|
#include "vips.h"
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type urlOptions map[string][]string
|
|
|
|
type imageType int
|
|
|
|
const (
|
|
imageTypeUnknown = imageType(C.UNKNOWN)
|
|
imageTypeJPEG = imageType(C.JPEG)
|
|
imageTypePNG = imageType(C.PNG)
|
|
imageTypeWEBP = imageType(C.WEBP)
|
|
imageTypeGIF = imageType(C.GIF)
|
|
imageTypeICO = imageType(C.ICO)
|
|
imageTypeSVG = imageType(C.SVG)
|
|
)
|
|
|
|
type processingHeaders struct {
|
|
Accept string
|
|
Width string
|
|
ViewportWidth string
|
|
DPR string
|
|
}
|
|
|
|
var imageTypes = map[string]imageType{
|
|
"jpeg": imageTypeJPEG,
|
|
"jpg": imageTypeJPEG,
|
|
"png": imageTypePNG,
|
|
"webp": imageTypeWEBP,
|
|
"gif": imageTypeGIF,
|
|
"ico": imageTypeICO,
|
|
"svg": imageTypeSVG,
|
|
}
|
|
|
|
type gravityType int
|
|
|
|
const (
|
|
gravityCenter gravityType = iota
|
|
gravityNorth
|
|
gravityEast
|
|
gravitySouth
|
|
gravityWest
|
|
gravityNorthWest
|
|
gravityNorthEast
|
|
gravitySouthWest
|
|
gravitySouthEast
|
|
gravitySmart
|
|
gravityFocusPoint
|
|
)
|
|
|
|
var gravityTypes = map[string]gravityType{
|
|
"ce": gravityCenter,
|
|
"no": gravityNorth,
|
|
"ea": gravityEast,
|
|
"so": gravitySouth,
|
|
"we": gravityWest,
|
|
"nowe": gravityNorthWest,
|
|
"noea": gravityNorthEast,
|
|
"sowe": gravitySouthWest,
|
|
"soea": gravitySouthEast,
|
|
"sm": gravitySmart,
|
|
"fp": gravityFocusPoint,
|
|
}
|
|
|
|
type gravityOptions struct {
|
|
Type gravityType
|
|
X, Y float64
|
|
}
|
|
|
|
type resizeType int
|
|
|
|
const (
|
|
resizeFit resizeType = iota
|
|
resizeFill
|
|
resizeCrop
|
|
)
|
|
|
|
var resizeTypes = map[string]resizeType{
|
|
"fit": resizeFit,
|
|
"fill": resizeFill,
|
|
"crop": resizeCrop,
|
|
}
|
|
|
|
type rgbColor struct{ R, G, B uint8 }
|
|
|
|
var hexColorRegex = regexp.MustCompile("^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
|
|
const (
|
|
hexColorLongFormat = "%02x%02x%02x"
|
|
hexColorShortFormat = "%1x%1x%1x"
|
|
)
|
|
|
|
type watermarkOptions struct {
|
|
Enabled bool
|
|
Opacity float64
|
|
Replicate bool
|
|
Gravity gravityType
|
|
OffsetX int
|
|
OffsetY int
|
|
Scale float64
|
|
}
|
|
|
|
type processingOptions struct {
|
|
Resize resizeType
|
|
Width int
|
|
Height int
|
|
Dpr float64
|
|
Gravity gravityOptions
|
|
Enlarge bool
|
|
Expand bool
|
|
Format imageType
|
|
Quality int
|
|
Flatten bool
|
|
Background rgbColor
|
|
Blur float32
|
|
Sharpen float32
|
|
|
|
CacheBuster string
|
|
|
|
Watermark watermarkOptions
|
|
|
|
UsedPresets []string
|
|
}
|
|
|
|
const (
|
|
imageURLCtxKey = ctxKey("imageUrl")
|
|
processingOptionsCtxKey = ctxKey("processingOptions")
|
|
urlTokenPlain = "plain"
|
|
maxClientHintDPR = 8
|
|
|
|
msgForbidden = "Forbidden"
|
|
msgInvalidURL = "Invalid URL"
|
|
)
|
|
|
|
var (
|
|
errInvalidImageURL = errors.New("Invalid image url")
|
|
errInvalidURLEncoding = errors.New("Invalid url encoding")
|
|
errResultingImageFormatIsNotSupported = errors.New("Resulting image format is not supported")
|
|
errInvalidPath = newError(404, "Invalid path", msgInvalidURL)
|
|
)
|
|
|
|
func (it imageType) String() string {
|
|
for k, v := range imageTypes {
|
|
if v == it {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (gt gravityType) String() string {
|
|
for k, v := range gravityTypes {
|
|
if v == gt {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (rt resizeType) String() string {
|
|
for k, v := range resizeTypes {
|
|
if v == rt {
|
|
return k
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func decodeBase64URL(parts []string) (string, string, error) {
|
|
var format string
|
|
|
|
urlParts := strings.Split(strings.Join(parts, ""), ".")
|
|
|
|
if len(urlParts) > 2 {
|
|
return "", "", errInvalidURLEncoding
|
|
}
|
|
|
|
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
|
|
format = urlParts[1]
|
|
}
|
|
|
|
imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "="))
|
|
if err != nil {
|
|
return "", "", errInvalidURLEncoding
|
|
}
|
|
|
|
fullURL := fmt.Sprintf("%s%s", conf.BaseURL, string(imageURL))
|
|
|
|
if _, err := url.ParseRequestURI(fullURL); err != nil {
|
|
return "", "", errInvalidImageURL
|
|
}
|
|
|
|
return fullURL, format, nil
|
|
}
|
|
|
|
func decodePlainURL(parts []string) (string, string, error) {
|
|
var format string
|
|
|
|
urlParts := strings.Split(strings.Join(parts, "/"), "@")
|
|
|
|
if len(urlParts) > 2 {
|
|
return "", "", errInvalidURLEncoding
|
|
}
|
|
|
|
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
|
|
format = urlParts[1]
|
|
}
|
|
|
|
if unescaped, err := url.PathUnescape(urlParts[0]); err == nil {
|
|
fullURL := fmt.Sprintf("%s%s", conf.BaseURL, unescaped)
|
|
if _, err := url.ParseRequestURI(fullURL); err == nil {
|
|
return fullURL, format, nil
|
|
}
|
|
}
|
|
|
|
return "", "", errInvalidImageURL
|
|
}
|
|
|
|
func decodeURL(parts []string) (string, string, error) {
|
|
if len(parts) == 0 {
|
|
return "", "", errInvalidURLEncoding
|
|
}
|
|
|
|
if parts[0] == urlTokenPlain && len(parts) > 1 {
|
|
return decodePlainURL(parts[1:])
|
|
}
|
|
|
|
return decodeBase64URL(parts)
|
|
}
|
|
|
|
func applyWidthOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid width arguments: %v", args)
|
|
}
|
|
|
|
if w, err := strconv.Atoi(args[0]); err == nil && w >= 0 {
|
|
po.Width = w
|
|
} else {
|
|
return fmt.Errorf("Invalid width: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyHeightOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid height arguments: %v", args)
|
|
}
|
|
|
|
if h, err := strconv.Atoi(args[0]); err == nil && po.Height >= 0 {
|
|
po.Height = h
|
|
} else {
|
|
return fmt.Errorf("Invalid height: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyEnlargeOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid enlarge arguments: %v", args)
|
|
}
|
|
|
|
po.Enlarge = args[0] != "0"
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyExtendOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid expand arguments: %v", args)
|
|
}
|
|
|
|
po.Expand = args[0] != "0"
|
|
|
|
return nil
|
|
}
|
|
|
|
func applySizeOption(po *processingOptions, args []string) (err error) {
|
|
if len(args) > 4 {
|
|
return fmt.Errorf("Invalid size arguments: %v", args)
|
|
}
|
|
|
|
if len(args) >= 1 && len(args[0]) > 0 {
|
|
if err = applyWidthOption(po, args[0:1]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if len(args) >= 2 && len(args[1]) > 0 {
|
|
if err = applyHeightOption(po, args[1:2]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if len(args) >= 3 && len(args[2]) > 0 {
|
|
if err = applyEnlargeOption(po, args[2:3]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if len(args) == 4 && len(args[3]) > 0 {
|
|
if err = applyExtendOption(po, args[3:4]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyResizingTypeOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid resizing type arguments: %v", args)
|
|
}
|
|
|
|
if r, ok := resizeTypes[args[0]]; ok {
|
|
po.Resize = r
|
|
} else {
|
|
return fmt.Errorf("Invalid resize type: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyResizeOption(po *processingOptions, args []string) error {
|
|
if len(args) > 5 {
|
|
return fmt.Errorf("Invalid resize arguments: %v", args)
|
|
}
|
|
|
|
if len(args[0]) > 0 {
|
|
if err := applyResizingTypeOption(po, args[0:1]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(args) > 1 {
|
|
if err := applySizeOption(po, args[1:]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyDprOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid dpr arguments: %v", args)
|
|
}
|
|
|
|
if d, err := strconv.ParseFloat(args[0], 64); err == nil || (d > 0 && d != 1) {
|
|
po.Dpr = d
|
|
} else {
|
|
return fmt.Errorf("Invalid dpr: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyGravityOption(po *processingOptions, args []string) error {
|
|
if g, ok := gravityTypes[args[0]]; ok {
|
|
po.Gravity.Type = g
|
|
} else {
|
|
return fmt.Errorf("Invalid gravity: %s", args[0])
|
|
}
|
|
|
|
if po.Gravity.Type == gravityFocusPoint {
|
|
if len(args) != 3 {
|
|
return fmt.Errorf("Invalid gravity arguments: %v", args)
|
|
}
|
|
|
|
if x, err := strconv.ParseFloat(args[1], 64); err == nil && x >= 0 && x <= 1 {
|
|
po.Gravity.X = x
|
|
} else {
|
|
return fmt.Errorf("Invalid gravity X: %s", args[1])
|
|
}
|
|
|
|
if y, err := strconv.ParseFloat(args[2], 64); err == nil && y >= 0 && y <= 1 {
|
|
po.Gravity.Y = y
|
|
} else {
|
|
return fmt.Errorf("Invalid gravity Y: %s", args[2])
|
|
}
|
|
} else if len(args) > 1 {
|
|
return fmt.Errorf("Invalid gravity arguments: %v", args)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyQualityOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid quality arguments: %v", args)
|
|
}
|
|
|
|
if q, err := strconv.Atoi(args[0]); err == nil && q > 0 && q <= 100 {
|
|
po.Quality = q
|
|
} else {
|
|
return fmt.Errorf("Invalid quality: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
case 3:
|
|
po.Flatten = true
|
|
|
|
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])
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyBlurOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid blur arguments: %v", args)
|
|
}
|
|
|
|
if b, err := strconv.ParseFloat(args[0], 32); err == nil || b >= 0 {
|
|
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)
|
|
}
|
|
|
|
if s, err := strconv.ParseFloat(args[0], 32); err == nil || s >= 0 {
|
|
po.Sharpen = float32(s)
|
|
} else {
|
|
return fmt.Errorf("Invalid sharpen: %s", args[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyPresetOption(po *processingOptions, args []string) error {
|
|
for _, preset := range args {
|
|
if p, ok := conf.Presets[preset]; ok {
|
|
if po.isPresetUsed(preset) {
|
|
logWarning("Recursive preset usage is detected: %s", preset)
|
|
continue
|
|
}
|
|
|
|
po.presetUsed(preset)
|
|
|
|
if err := applyProcessingOptions(po, p); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return fmt.Errorf("Unknown asset: %s", preset)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyWatermarkOption(po *processingOptions, args []string) error {
|
|
if len(args) > 7 {
|
|
return fmt.Errorf("Invalid watermark arguments: %v", args)
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
if len(args) > 1 && len(args[1]) > 0 {
|
|
if args[1] == "re" {
|
|
po.Watermark.Replicate = true
|
|
} else if g, ok := gravityTypes[args[1]]; ok && g != gravityFocusPoint && g != gravitySmart {
|
|
po.Watermark.Gravity = g
|
|
} else {
|
|
return fmt.Errorf("Invalid watermark position: %s", args[1])
|
|
}
|
|
}
|
|
|
|
if len(args) > 2 && len(args[2]) > 0 {
|
|
if x, err := strconv.Atoi(args[2]); err == nil {
|
|
po.Watermark.OffsetX = x
|
|
} else {
|
|
return fmt.Errorf("Invalid watermark X offset: %s", args[2])
|
|
}
|
|
}
|
|
|
|
if len(args) > 3 && len(args[3]) > 0 {
|
|
if y, err := strconv.Atoi(args[3]); err == nil {
|
|
po.Watermark.OffsetY = y
|
|
} else {
|
|
return fmt.Errorf("Invalid watermark Y offset: %s", args[3])
|
|
}
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyFormatOption(po *processingOptions, args []string) error {
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("Invalid format arguments: %v", args)
|
|
}
|
|
|
|
if conf.EnforceWebp && po.Format == imageTypeWEBP {
|
|
// Webp is enforced and already set as format
|
|
return nil
|
|
}
|
|
|
|
if f, ok := imageTypes[args[0]]; ok {
|
|
po.Format = f
|
|
} else {
|
|
return fmt.Errorf("Invalid image format: %s", args[0])
|
|
}
|
|
|
|
if !vipsTypeSupportSave[po.Format] {
|
|
return errResultingImageFormatIsNotSupported
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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 applyProcessingOption(po *processingOptions, name string, args []string) error {
|
|
switch name {
|
|
case "format", "f", "ext":
|
|
if err := applyFormatOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "resize", "rs":
|
|
if err := applyResizeOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "resizing_type", "rt":
|
|
if err := applyResizingTypeOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "size", "s":
|
|
if err := applySizeOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "width", "w":
|
|
if err := applyWidthOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "height", "h":
|
|
if err := applyHeightOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "enlarge", "el":
|
|
if err := applyEnlargeOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "extend", "ex":
|
|
if err := applyExtendOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "dpr":
|
|
if err := applyDprOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "gravity", "g":
|
|
if err := applyGravityOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "quality", "q":
|
|
if err := applyQualityOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "background", "bg":
|
|
if err := applyBackgroundOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "blur", "bl":
|
|
if err := applyBlurOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "sharpen", "sh":
|
|
if err := applySharpenOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "watermark", "wm":
|
|
if err := applyWatermarkOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "preset", "pr":
|
|
if err := applyPresetOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
case "cachebuster", "cb":
|
|
if err := applyCacheBusterOption(po, args); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("Unknown processing option: %s", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyProcessingOptions(po *processingOptions, options urlOptions) error {
|
|
for name, args := range options {
|
|
if err := applyProcessingOption(po, name, args); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseURLOptions(opts []string) (urlOptions, []string) {
|
|
parsed := make(urlOptions)
|
|
urlStart := len(opts) + 1
|
|
|
|
for i, opt := range opts {
|
|
args := strings.Split(opt, ":")
|
|
|
|
if len(args) == 1 {
|
|
urlStart = i
|
|
break
|
|
}
|
|
|
|
parsed[args[0]] = args[1:]
|
|
}
|
|
|
|
var rest []string
|
|
|
|
if urlStart < len(opts) {
|
|
rest = opts[urlStart:]
|
|
} else {
|
|
rest = []string{}
|
|
}
|
|
|
|
return parsed, rest
|
|
}
|
|
|
|
func defaultProcessingOptions(headers *processingHeaders) (*processingOptions, error) {
|
|
var err error
|
|
|
|
po := processingOptions{
|
|
Resize: resizeFit,
|
|
Width: 0,
|
|
Height: 0,
|
|
Gravity: gravityOptions{Type: gravityCenter},
|
|
Enlarge: false,
|
|
Quality: conf.Quality,
|
|
Format: imageTypeUnknown,
|
|
Background: rgbColor{255, 255, 255},
|
|
Blur: 0,
|
|
Sharpen: 0,
|
|
Dpr: 1,
|
|
Watermark: watermarkOptions{Opacity: 1, Replicate: false, Gravity: gravityCenter},
|
|
UsedPresets: make([]string, 0, len(conf.Presets)),
|
|
}
|
|
|
|
if (conf.EnableWebpDetection || conf.EnforceWebp) && strings.Contains(headers.Accept, "image/webp") {
|
|
po.Format = imageTypeWEBP
|
|
}
|
|
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 {
|
|
if dpr, err := strconv.ParseFloat(headers.DPR, 64); err == nil || (dpr > 0 && dpr <= maxClientHintDPR) {
|
|
po.Dpr = dpr
|
|
}
|
|
}
|
|
if _, ok := conf.Presets["default"]; ok {
|
|
err = applyPresetOption(&po, []string{"default"})
|
|
}
|
|
|
|
return &po, err
|
|
}
|
|
|
|
func parsePathAdvanced(parts []string, headers *processingHeaders) (string, *processingOptions, error) {
|
|
po, err := defaultProcessingOptions(headers)
|
|
if err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
options, urlParts := parseURLOptions(parts)
|
|
|
|
if err := applyProcessingOptions(po, options); err != nil {
|
|
return "", po, 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
|
|
}
|
|
|
|
func parsePathBasic(parts []string, headers *processingHeaders) (string, *processingOptions, error) {
|
|
var err error
|
|
|
|
if len(parts) < 6 {
|
|
return "", nil, errInvalidPath
|
|
}
|
|
|
|
po, err := defaultProcessingOptions(headers)
|
|
if err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
po.Resize = resizeTypes[parts[0]]
|
|
|
|
if err = applyWidthOption(po, parts[1:2]); err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
if err = applyHeightOption(po, parts[2:3]); err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
if err = applyGravityOption(po, strings.Split(parts[3], ":")); err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
if err = applyEnlargeOption(po, parts[4:5]); err != nil {
|
|
return "", po, err
|
|
}
|
|
|
|
url, extension, err := decodeURL(parts[5:])
|
|
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
|
|
}
|
|
|
|
func parsePath(ctx context.Context, r *http.Request) (context.Context, error) {
|
|
path := r.URL.RawPath
|
|
if len(path) == 0 {
|
|
path = r.URL.Path
|
|
}
|
|
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
|
|
|
if len(parts) < 3 {
|
|
return ctx, errInvalidPath
|
|
}
|
|
|
|
if !conf.AllowInsecure {
|
|
if err := validatePath(parts[0], strings.TrimPrefix(path, fmt.Sprintf("/%s", parts[0]))); err != nil {
|
|
return ctx, newError(403, err.Error(), msgForbidden)
|
|
}
|
|
}
|
|
|
|
headers := &processingHeaders{
|
|
Accept: r.Header.Get("Accept"),
|
|
Width: r.Header.Get("Width"),
|
|
ViewportWidth: r.Header.Get("Viewport-Width"),
|
|
DPR: r.Header.Get("DPR"),
|
|
}
|
|
|
|
var imageURL string
|
|
var po *processingOptions
|
|
var err error
|
|
|
|
if _, ok := resizeTypes[parts[1]]; ok {
|
|
imageURL, po, err = parsePathBasic(parts[1:], headers)
|
|
} else {
|
|
imageURL, po, err = parsePathAdvanced(parts[1:], headers)
|
|
}
|
|
|
|
if err != nil {
|
|
return ctx, newError(404, err.Error(), msgInvalidURL)
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, imageURLCtxKey, imageURL)
|
|
ctx = context.WithValue(ctx, processingOptionsCtxKey, po)
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
func getImageURL(ctx context.Context) string {
|
|
return ctx.Value(imageURLCtxKey).(string)
|
|
}
|
|
|
|
func getProcessingOptions(ctx context.Context) *processingOptions {
|
|
return ctx.Value(processingOptionsCtxKey).(*processingOptions)
|
|
}
|