Global refactoring

This commit is contained in:
DarthSim
2021-04-26 17:52:50 +06:00
parent 6cf263cbb1
commit 2ea1466d96
100 changed files with 5405 additions and 4481 deletions

52
options/gravity_type.go Normal file
View File

@@ -0,0 +1,52 @@
package options
import "fmt"
type GravityType int
const (
GravityUnknown GravityType = iota
GravityCenter
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,
}
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
}

69
options/presets.go Normal file
View File

@@ -0,0 +1,69 @@
package options
import (
"fmt"
"strings"
)
var presets map[string]urlOptions
func ParsePresets(presetStrs []string) error {
for _, presetStr := range presetStrs {
if err := parsePreset(presetStr); err != nil {
return err
}
}
return nil
}
func parsePreset(presetStr string) error {
presetStr = strings.Trim(presetStr, " ")
if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
return nil
}
parts := strings.Split(presetStr, "=")
if len(parts) != 2 {
return fmt.Errorf("Invalid preset string: %s", presetStr)
}
name := strings.Trim(parts[0], " ")
if len(name) == 0 {
return fmt.Errorf("Empty preset name: %s", presetStr)
}
value := strings.Trim(parts[1], " ")
if len(value) == 0 {
return fmt.Errorf("Empty preset value: %s", presetStr)
}
optsStr := strings.Split(value, "/")
opts, rest := parseURLOptions(optsStr)
if len(rest) > 0 {
return fmt.Errorf("Invalid preset value: %s", presetStr)
}
if presets == nil {
presets = make(map[string]urlOptions)
}
presets[name] = opts
return nil
}
func ValidatePresets() error {
var po ProcessingOptions
for name, opts := range presets {
if err := applyURLOptions(&po, opts); err != nil {
return fmt.Errorf("Error in preset `%s`: %s", name, err)
}
}
return nil
}

106
options/presets_test.go Normal file
View File

@@ -0,0 +1,106 @@
package options
import (
"fmt"
"testing"
"github.com/imgproxy/imgproxy/v2/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type PresetsTestSuite struct{ suite.Suite }
func (s *PresetsTestSuite) SetupTest() {
config.Reset()
// Reset presets
presets = make(map[string]urlOptions)
}
func (s *PresetsTestSuite) TestParsePreset() {
err := parsePreset("test=resize:fit:100:200/sharpen:2")
require.Nil(s.T(), err)
assert.Equal(s.T(), urlOptions{
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
}, presets["test"])
}
func (s *PresetsTestSuite) TestParsePresetInvalidString() {
presetStr := "resize:fit:100:200/sharpen:2"
err := parsePreset(presetStr)
assert.Equal(s.T(), fmt.Errorf("Invalid preset string: %s", presetStr), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestParsePresetEmptyName() {
presetStr := "=resize:fit:100:200/sharpen:2"
err := parsePreset(presetStr)
assert.Equal(s.T(), fmt.Errorf("Empty preset name: %s", presetStr), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
presetStr := "test="
err := parsePreset(presetStr)
assert.Equal(s.T(), fmt.Errorf("Empty preset value: %s", presetStr), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
presetStr := "test=resize:fit:100:200/sharpen:2/blur"
err := parsePreset(presetStr)
assert.Equal(s.T(), fmt.Errorf("Invalid preset value: %s", presetStr), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestParsePresetEmptyString() {
err := parsePreset(" ")
assert.Nil(s.T(), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestParsePresetComment() {
err := parsePreset("# test=resize:fit:100:200/sharpen:2")
assert.Nil(s.T(), err)
assert.Empty(s.T(), presets)
}
func (s *PresetsTestSuite) TestValidatePresets() {
presets = map[string]urlOptions{
"test": urlOptions{
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
},
}
err := ValidatePresets()
assert.Nil(s.T(), err)
}
func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
presets = map[string]urlOptions{
"test": urlOptions{
urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
},
}
err := ValidatePresets()
assert.Error(s.T(), err)
}
func TestPresets(t *testing.T) {
suite.Run(t, new(PresetsTestSuite))
}

View File

@@ -0,0 +1,968 @@
package options
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/imgproxy/imgproxy/v2/config"
"github.com/imgproxy/imgproxy/v2/ierrors"
"github.com/imgproxy/imgproxy/v2/imagetype"
"github.com/imgproxy/imgproxy/v2/structdiff"
"github.com/imgproxy/imgproxy/v2/vips"
)
const maxClientHintDPR = 8
var errExpiredURL = errors.New("Expired URL")
type GravityOptions struct {
Type GravityType
X, Y float64
}
type ExtendOptions struct {
Enabled bool
Gravity GravityOptions
}
type CropOptions struct {
Width float64
Height float64
Gravity GravityOptions
}
type PaddingOptions struct {
Enabled bool
Top int
Right int
Bottom int
Left int
}
type TrimOptions struct {
Enabled bool
Threshold float64
Smart bool
Color vips.Color
EqualHor bool
EqualVer bool
}
type WatermarkOptions struct {
Enabled bool
Opacity float64
Replicate bool
Gravity GravityOptions
Scale float64
}
type ProcessingOptions struct {
ResizingType ResizeType
Width int
Height int
MinWidth int
MinHeight int
Dpr float64
Gravity GravityOptions
Enlarge bool
Extend ExtendOptions
Crop CropOptions
Padding PaddingOptions
Trim TrimOptions
Rotate int
Format imagetype.Type
Quality int
MaxBytes int
Flatten bool
Background vips.Color
Blur float32
Sharpen float32
StripMetadata bool
StripColorProfile bool
AutoRotate bool
SkipProcessingFormats []imagetype.Type
CacheBuster string
Watermark WatermarkOptions
PreferWebP bool
EnforceWebP bool
PreferAvif bool
EnforceAvif bool
Filename string
UsedPresets []string
}
var (
_newProcessingOptions ProcessingOptions
newProcessingOptionsOnce sync.Once
)
func NewProcessingOptions() *ProcessingOptions {
newProcessingOptionsOnce.Do(func() {
_newProcessingOptions = ProcessingOptions{
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},
Rotate: 0,
Quality: 0,
MaxBytes: 0,
Format: imagetype.Unknown,
Background: vips.Color{R: 255, G: 255, B: 255},
Blur: 0,
Sharpen: 0,
Dpr: 1,
Watermark: WatermarkOptions{Opacity: 1, Replicate: false, Gravity: GravityOptions{Type: GravityCenter}},
StripMetadata: config.StripMetadata,
StripColorProfile: config.StripColorProfile,
AutoRotate: config.AutoRotate,
}
})
po := _newProcessingOptions
po.SkipProcessingFormats = append([]imagetype.Type(nil), config.SkipProcessingFormats...)
po.UsedPresets = make([]string, 0, len(config.Presets))
return &po
}
func (po *ProcessingOptions) GetQuality() int {
q := po.Quality
if q == 0 {
q = config.FormatQuality[po.Format]
}
if q == 0 {
q = config.Quality
}
return q
}
func (po *ProcessingOptions) isPresetUsed(name string) bool {
for _, usedName := range po.UsedPresets {
if usedName == name {
return true
}
}
return false
}
func (po *ProcessingOptions) Diff() structdiff.Entries {
return structdiff.Diff(NewProcessingOptions(), po)
}
func (po *ProcessingOptions) String() string {
return po.Diff().String()
}
func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
return po.Diff().MarshalJSON()
}
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)
}
return nil
}
func parseBoolOption(str string) bool {
b, err := strconv.ParseBool(str)
if err != nil {
log.Warningf("`%s` is not a valid boolean value. Treated as false", str)
}
return b
}
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 {
nArgs := len(args)
if nArgs > 3 {
return fmt.Errorf("Invalid gravity arguments: %v", args)
}
if t, ok := gravityTypes[args[0]]; ok {
g.Type = t
} else {
return fmt.Errorf("Invalid gravity: %s", args[0])
}
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)
}
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])
}
}
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])
}
}
return nil
}
func applyWidthOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid width arguments: %v", args)
}
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)
}
return parseDimension(&po.Height, "height", args[0])
}
func applyMinWidthOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid min width arguments: %v", args)
}
return parseDimension(&po.MinWidth, "min width", args[0])
}
func applyMinHeightOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid min height arguments: %v", args)
}
return parseDimension(&po.MinHeight, " min height", args[0])
}
func applyEnlargeOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid enlarge arguments: %v", args)
}
po.Enlarge = parseBoolOption(args[0])
return nil
}
func applyExtendOption(po *ProcessingOptions, args []string) error {
if len(args) > 4 {
return fmt.Errorf("Invalid extend arguments: %v", args)
}
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")
}
}
return nil
}
func applySizeOption(po *ProcessingOptions, args []string) (err error) {
if len(args) > 7 {
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:]); 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.ResizingType = r
} else {
return fmt.Errorf("Invalid resize type: %s", args[0])
}
return nil
}
func applyResizeOption(po *ProcessingOptions, args []string) error {
if len(args) > 8 {
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 {
po.Dpr = d
} else {
return fmt.Errorf("Invalid dpr: %s", args[0])
}
return nil
}
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)
}
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])
}
if len(args) > 1 {
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])
}
}
if len(args) > 2 {
return parseGravity(&po.Crop.Gravity, args[2:])
}
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
}
func applyTrimOption(po *ProcessingOptions, args []string) error {
nArgs := len(args)
if nArgs > 4 {
return fmt.Errorf("Invalid trim arguments: %v", args)
}
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 := vips.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])
}
return nil
}
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
}
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 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
}
func applyBackgroundOption(po *ProcessingOptions, args []string) error {
switch len(args) {
case 1:
if len(args[0]) == 0 {
po.Flatten = false
} else if c, err := vips.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 := presets[preset]; ok {
if po.isPresetUsed(preset) {
log.Warningf("Recursive preset usage is detected: %s", preset)
continue
}
po.UsedPresets = append(po.UsedPresets, preset)
if err := applyURLOptions(po, p); err != nil {
return err
}
} else {
return fmt.Errorf("Unknown preset: %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.Type = 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.Gravity.X = float64(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.Gravity.Y = float64(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 f, ok := imagetype.Types[args[0]]; ok {
po.Format = f
} else {
return fmt.Errorf("Invalid image format: %s", args[0])
}
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 applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) error {
for _, format := range args {
if f, ok := imagetype.Types[format]; ok {
po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
} else {
return fmt.Errorf("Invalid image format in skip processing: %s", format)
}
}
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 applyExpiresOption(po *ProcessingOptions, args []string) error {
if len(args) > 1 {
return fmt.Errorf("Invalid expires arguments: %v", args)
}
timestamp, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("Invalid expires argument: %v", args[0])
}
if timestamp > 0 && timestamp < time.Now().Unix() {
return errExpiredURL
}
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
}
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
}
func applyURLOption(po *ProcessingOptions, name string, args []string) error {
switch name {
case "resize", "rs":
return applyResizeOption(po, args)
case "size", "s":
return applySizeOption(po, args)
case "resizing_type", "rt":
return applyResizingTypeOption(po, args)
case "width", "w":
return applyWidthOption(po, args)
case "height", "h":
return applyHeightOption(po, args)
case "min-width", "mw":
return applyMinWidthOption(po, args)
case "min-height", "mh":
return applyMinHeightOption(po, args)
case "dpr":
return applyDprOption(po, args)
case "enlarge", "el":
return applyEnlargeOption(po, args)
case "extend", "ex":
return applyExtendOption(po, args)
case "gravity", "g":
return applyGravityOption(po, args)
case "crop", "c":
return applyCropOption(po, args)
case "trim", "t":
return applyTrimOption(po, args)
case "padding", "pd":
return applyPaddingOption(po, args)
case "auto_rotate", "ar":
return applyAutoRotateOption(po, args)
case "rotate", "rot":
return applyRotateOption(po, args)
case "background", "bg":
return applyBackgroundOption(po, args)
case "blur", "bl":
return applyBlurOption(po, args)
case "sharpen", "sh":
return applySharpenOption(po, args)
case "watermark", "wm":
return applyWatermarkOption(po, args)
case "strip_metadata", "sm":
return applyStripMetadataOption(po, args)
case "strip_color_profile", "scp":
return applyStripColorProfileOption(po, args)
// Saving options
case "quality", "q":
return applyQualityOption(po, args)
case "max_bytes", "mb":
return applyMaxBytesOption(po, args)
case "format", "f", "ext":
return applyFormatOption(po, args)
// Handling options
case "skip_processing", "skp":
return applySkipProcessingFormatsOption(po, args)
case "cachebuster", "cb":
return applyCacheBusterOption(po, args)
case "expires", "exp":
return applyExpiresOption(po, args)
case "filename", "fn":
return applyFilenameOption(po, args)
// Presets
case "preset", "pr":
return applyPresetOption(po, args)
}
return fmt.Errorf("Unknown processing option: %s", name)
}
func applyURLOptions(po *ProcessingOptions, options urlOptions) error {
for _, opt := range options {
if err := applyURLOption(po, opt.Name, opt.Args); err != nil {
return err
}
}
return nil
}
func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
po := NewProcessingOptions()
headerAccept := headers.Get("Accept")
if strings.Contains(headerAccept, "image/webp") {
po.PreferWebP = config.EnableWebpDetection || config.EnforceWebp
po.EnforceWebP = config.EnforceWebp
}
if strings.Contains(headerAccept, "image/avif") {
po.PreferAvif = config.EnableAvifDetection || config.EnforceAvif
po.EnforceAvif = config.EnforceAvif
}
if config.EnableClientHints {
if headerViewportWidth := headers.Get("Viewport-Width"); len(headerViewportWidth) > 0 {
if vw, err := strconv.Atoi(headerViewportWidth); err == nil {
po.Width = vw
}
}
if headerWidth := headers.Get("Width"); len(headerWidth) > 0 {
if w, err := strconv.Atoi(headerWidth); err == nil {
po.Width = w
}
}
if headerDPR := headers.Get("DPR"); len(headerDPR) > 0 {
if dpr, err := strconv.ParseFloat(headerDPR, 64); err == nil && (dpr > 0 && dpr <= maxClientHintDPR) {
po.Dpr = dpr
}
}
}
if _, ok := presets["default"]; ok {
if err := applyPresetOption(po, []string{"default"}); err != nil {
return po, err
}
}
return po, nil
}
func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
po, err := defaultProcessingOptions(headers)
if err != nil {
return nil, "", err
}
options, urlParts := parseURLOptions(parts)
if err = applyURLOptions(po, options); err != nil {
return nil, "", err
}
url, extension, err := DecodeURL(urlParts)
if err != nil {
return nil, "", err
}
if len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}
func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
po, err := defaultProcessingOptions(headers)
if err != nil {
return nil, "", 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 nil, "", err
}
if len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}
func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
if path == "" || path == "/" {
return nil, "", ierrors.New(404, fmt.Sprintf("Invalid path: %s", path), "Invalid URL")
}
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
var (
imageURL string
po *ProcessingOptions
err error
)
if config.OnlyPresets {
po, imageURL, err = parsePathPresets(parts, headers)
} else {
po, imageURL, err = parsePathOptions(parts, headers)
}
if err != nil {
return nil, "", ierrors.New(404, err.Error(), "Invalid URL")
}
return po, imageURL, nil
}

View File

@@ -0,0 +1,596 @@
package options
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/imgproxy/imgproxy/v2/config"
"github.com/imgproxy/imgproxy/v2/imagetype"
)
type ProcessingOptionsTestSuite struct{ suite.Suite }
func (s *ProcessingOptionsTestSuite) SetupTest() {
config.Reset()
// Reset presets
presets = make(map[string]urlOptions)
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), originURL, imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), originURL, imageURL)
assert.Equal(s.T(), imagetype.Unknown, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
config.BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
originURL := "http://images.dev/lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), originURL, imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
originURL := "http://images.dev/lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), originURL, imageURL)
assert.Equal(s.T(), imagetype.Unknown, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), originURL, imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
config.BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
config.BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
assert.Equal(s.T(), imagetype.PNG, po.Format)
}
// func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
// config.AllowedSources = []string{"local://", "http://images.dev/"}
// path := "/plain/http://images.dev/lorem/ipsum.jpg"
// _, _, err := ParsePath(path, make(http.Header))
// require.Nil(s.T(), err)
// }
// func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
// config.AllowedSources = []string{"local://", "http://images.dev/"}
// path := "/plain/s3://images/lorem/ipsum.jpg"
// _, _, err := ParsePath(path, make(http.Header))
// require.Error(s.T(), err)
// }
func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), imagetype.WEBP, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), ResizeFill, po.ResizingType)
assert.Equal(s.T(), 100, po.Width)
assert.Equal(s.T(), 200, po.Height)
assert.True(s.T(), po.Enlarge)
}
func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), ResizeFill, po.ResizingType)
}
func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), 100, po.Width)
assert.Equal(s.T(), 200, po.Height)
assert.True(s.T(), po.Enlarge)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), 100, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), 100, po.Height)
}
func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.True(s.T(), po.Enlarge)
}
func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), true, po.Extend.Enabled)
assert.Equal(s.T(), GravitySouth, po.Extend.Gravity.Type)
assert.Equal(s.T(), 10.0, po.Extend.Gravity.X)
assert.Equal(s.T(), 20.0, po.Extend.Gravity.Y)
}
func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), GravitySouthEast, po.Gravity.Type)
}
func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocuspoint() {
path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), GravityFocusPoint, po.Gravity.Type)
assert.Equal(s.T(), 0.5, po.Gravity.X)
assert.Equal(s.T(), 0.75, po.Gravity.Y)
}
func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), 55, po.Quality)
}
func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.True(s.T(), po.Flatten)
assert.Equal(s.T(), uint8(128), po.Background.R)
assert.Equal(s.T(), uint8(129), po.Background.G)
assert.Equal(s.T(), uint8(130), po.Background.B)
}
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.True(s.T(), po.Flatten)
assert.Equal(s.T(), uint8(0xff), po.Background.R)
assert.Equal(s.T(), uint8(0xdd), po.Background.G)
assert.Equal(s.T(), uint8(0xee), po.Background.B)
}
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.False(s.T(), po.Flatten)
}
func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), float32(0.2), po.Blur)
}
func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), float32(0.2), po.Sharpen)
}
func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), 2.0, po.Dpr)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.True(s.T(), po.Watermark.Enabled)
assert.Equal(s.T(), GravitySouthEast, po.Watermark.Gravity.Type)
assert.Equal(s.T(), 10.0, po.Watermark.Gravity.X)
assert.Equal(s.T(), 20.0, po.Watermark.Gravity.Y)
assert.Equal(s.T(), 0.6, po.Watermark.Scale)
}
func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
presets["test1"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "quality", Args: []string{"50"}},
}
path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), ResizeFill, po.ResizingType)
assert.Equal(s.T(), float32(0.2), po.Blur)
assert.Equal(s.T(), 50, po.Quality)
}
func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
presets["default"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "quality", Args: []string{"50"}},
}
path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), ResizeFill, po.ResizingType)
assert.Equal(s.T(), float32(0.2), po.Blur)
assert.Equal(s.T(), 70, po.Quality)
}
func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
presets["test1"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "quality", Args: []string{"50"}},
}
path := "/preset:test1:test2:test1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
require.ElementsMatch(s.T(), po.UsedPresets, []string{"test1", "test2"})
}
func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), "123", po.CacheBuster)
}
func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.True(s.T(), po.StripMetadata)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
config.EnableWebpDetection = true
path := "/plain/http://images.dev/lorem/ipsum.jpg"
headers := http.Header{"Accept": []string{"image/webp"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), true, po.PreferWebP)
assert.Equal(s.T(), false, po.EnforceWebP)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
config.EnforceWebp = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Accept": []string{"image/webp"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), true, po.PreferWebP)
assert.Equal(s.T(), true, po.EnforceWebP)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
config.EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 100, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 0, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
config.EnableClientHints = true
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 150, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeader() {
config.EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Viewport-Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 100, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderDisabled() {
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Viewport-Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 0, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathViewportWidthHeaderRedefine() {
config.EnableClientHints = true
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Viewport-Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 150, po.Width)
}
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
config.EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Dpr": []string{"2"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 2.0, po.Dpr)
}
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Dpr": []string{"2"}}
po, _, err := ParsePath(path, headers)
require.Nil(s.T(), err)
assert.Equal(s.T(), 1.0, po.Dpr)
}
// func (s *ProcessingOptionsTestSuite) TestParsePathSigned() {
// config.Keys = [][]byte{[]byte("test-key")}
// config.Salts = [][]byte{[]byte("test-salt")}
// path := "/HcvNognEV1bW6f8zRqxNYuOkV0IUf1xloRb57CzbT4g/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
// _, _, err := ParsePath(path, make(http.Header))
// require.Nil(s.T(), err)
// }
// func (s *ProcessingOptionsTestSuite) TestParsePathSignedInvalid() {
// config.Keys = [][]byte{[]byte("test-key")}
// config.Salts = [][]byte{[]byte("test-salt")}
// path := "/unsafe/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
// _, _, err := ParsePath(path, make(http.Header))
// require.Error(s.T(), err)
// assert.Equal(s.T(), signature.ErrInvalidSignature.Error(), err.Error())
// }
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
config.OnlyPresets = true
presets["test1"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "quality", Args: []string{"50"}},
}
path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), float32(0.2), po.Blur)
assert.Equal(s.T(), 50, po.Quality)
}
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), []imagetype.Type{imagetype.JPEG, imagetype.PNG}, po.SkipProcessingFormats)
}
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
require.Error(s.T(), err)
assert.Equal(s.T(), "Invalid image format in skip processing: bad_format", err.Error())
}
func (s *ProcessingOptionsTestSuite) TestParseExpires() {
path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
}
func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
require.Error(s.T(), err)
assert.Equal(s.T(), errExpiredURL.Error(), err.Error())
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
config.OnlyPresets = true
presets["test1"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "quality", Args: []string{"50"}},
}
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
require.Nil(s.T(), err)
assert.Equal(s.T(), float32(0.2), po.Blur)
assert.Equal(s.T(), 50, po.Quality)
assert.Equal(s.T(), originURL, imageURL)
}
func TestProcessingOptions(t *testing.T) {
suite.Run(t, new(ProcessingOptionsTestSuite))
}

39
options/resize_type.go Normal file
View File

@@ -0,0 +1,39 @@
package options
import "fmt"
type ResizeType int
const (
ResizeFit ResizeType = iota
ResizeFill
ResizeFillDown
ResizeForce
ResizeAuto
)
var resizeTypes = map[string]ResizeType{
"fit": ResizeFit,
"fill": ResizeFill,
"fill-down": ResizeFillDown,
"force": ResizeForce,
"auto": ResizeAuto,
}
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
}

81
options/url.go Normal file
View File

@@ -0,0 +1,81 @@
package options
import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"strings"
"github.com/imgproxy/imgproxy/v2/config"
)
const urlTokenPlain = "plain"
func decodeBase64URL(parts []string) (string, string, error) {
var format string
encoded := strings.Join(parts, "")
urlParts := strings.Split(encoded, ".")
if len(urlParts[0]) == 0 {
return "", "", errors.New("Image URL is empty")
}
if len(urlParts) > 2 {
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
}
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
format = urlParts[1]
}
imageURL, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(urlParts[0], "="))
if err != nil {
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
}
fullURL := fmt.Sprintf("%s%s", config.BaseURL, string(imageURL))
return fullURL, format, nil
}
func decodePlainURL(parts []string) (string, string, error) {
var format string
encoded := strings.Join(parts, "/")
urlParts := strings.Split(encoded, "@")
if len(urlParts[0]) == 0 {
return "", "", errors.New("Image URL is empty")
}
if len(urlParts) > 2 {
return "", "", fmt.Errorf("Multiple formats are specified: %s", encoded)
}
if len(urlParts) == 2 && len(urlParts[1]) > 0 {
format = urlParts[1]
}
unescaped, err := url.PathUnescape(urlParts[0])
if err != nil {
return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
}
fullURL := fmt.Sprintf("%s%s", config.BaseURL, unescaped)
return fullURL, format, nil
}
func DecodeURL(parts []string) (string, string, error) {
if len(parts) == 0 {
return "", "", errors.New("Image URL is empty")
}
if parts[0] == urlTokenPlain && len(parts) > 1 {
return decodePlainURL(parts[1:])
}
return decodeBase64URL(parts)
}

36
options/url_options.go Normal file
View File

@@ -0,0 +1,36 @@
package options
import "strings"
type urlOption struct {
Name string
Args []string
}
type urlOptions []urlOption
func parseURLOptions(opts []string) (urlOptions, []string) {
parsed := make(urlOptions, 0, len(opts))
urlStart := len(opts) + 1
for i, opt := range opts {
args := strings.Split(opt, ":")
if len(args) == 1 {
urlStart = i
break
}
parsed = append(parsed, urlOption{Name: args[0], Args: args[1:]})
}
var rest []string
if urlStart < len(opts) {
rest = opts[urlStart:]
} else {
rest = []string{}
}
return parsed, rest
}