123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- package options
- import (
- "log/slog"
- "maps"
- "net/http"
- "slices"
- "strconv"
- "strings"
- "time"
- "github.com/imgproxy/imgproxy/v3/ierrors"
- "github.com/imgproxy/imgproxy/v3/imagetype"
- "github.com/imgproxy/imgproxy/v3/imath"
- "github.com/imgproxy/imgproxy/v3/security"
- "github.com/imgproxy/imgproxy/v3/structdiff"
- "github.com/imgproxy/imgproxy/v3/vips"
- )
- const maxClientHintDPR = 8
- 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
- Position GravityOptions
- Scale float64
- }
- func (wo WatermarkOptions) ShouldReplicate() bool {
- return wo.Position.Type == GravityReplicate
- }
- type ProcessingOptions struct {
- defaultOptions *ProcessingOptions
- config *Config
- ResizingType ResizeType
- Width int
- Height int
- MinWidth int
- MinHeight int
- ZoomWidth float64
- ZoomHeight float64
- Dpr float64
- Gravity GravityOptions
- Enlarge bool
- Extend ExtendOptions
- ExtendAspectRatio ExtendOptions
- Crop CropOptions
- Padding PaddingOptions
- Trim TrimOptions
- Rotate int
- Format imagetype.Type
- Quality int
- FormatQuality map[imagetype.Type]int
- MaxBytes int
- Flatten bool
- Background vips.Color
- Blur float32
- Sharpen float32
- Pixelate int
- StripMetadata bool
- KeepCopyright bool
- StripColorProfile bool
- AutoRotate bool
- EnforceThumbnail bool
- SkipProcessingFormats []imagetype.Type
- CacheBuster string
- Expires *time.Time
- Watermark WatermarkOptions
- PreferWebP bool
- EnforceWebP bool
- PreferAvif bool
- EnforceAvif bool
- PreferJxl bool
- EnforceJxl bool
- Filename string
- ReturnAttachment bool
- Raw bool
- UsedPresets []string
- SecurityOptions security.Options
- }
- func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
- po := ProcessingOptions{
- config: config,
- ResizingType: ResizeFit,
- Width: 0,
- Height: 0,
- ZoomWidth: 1,
- ZoomHeight: 1,
- Gravity: GravityOptions{Type: GravityCenter},
- Enlarge: false,
- Extend: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
- ExtendAspectRatio: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
- Padding: PaddingOptions{Enabled: false},
- Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
- Rotate: 0,
- Quality: 0,
- FormatQuality: maps.Clone(config.FormatQuality),
- MaxBytes: 0,
- Format: imagetype.Unknown,
- Background: vips.Color{R: 255, G: 255, B: 255},
- Blur: 0,
- Sharpen: 0,
- Dpr: 1,
- Watermark: WatermarkOptions{Opacity: 1, Position: GravityOptions{Type: GravityCenter}},
- StripMetadata: config.StripMetadata,
- KeepCopyright: config.KeepCopyright,
- StripColorProfile: config.StripColorProfile,
- AutoRotate: config.AutoRotate,
- EnforceThumbnail: config.EnforceThumbnail,
- ReturnAttachment: config.ReturnAttachment,
- SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
- SecurityOptions: security.NewOptions(),
- }
- return &po
- }
- func (po *ProcessingOptions) GetQuality() int {
- q := po.Quality
- if q == 0 {
- q = po.FormatQuality[po.Format]
- }
- if q == 0 {
- q = po.config.Quality
- }
- return q
- }
- func (po *ProcessingOptions) Diff() structdiff.Entries {
- return structdiff.Diff(po.defaultOptions, po)
- }
- func (po *ProcessingOptions) String() string {
- return po.Diff().String()
- }
- func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
- return po.Diff().MarshalJSON()
- }
- func (po *ProcessingOptions) LogValue() slog.Value {
- return po.Diff().LogValue()
- }
- // Default returns the ProcessingOptions instance with defaults set
- func (po *ProcessingOptions) Default() *ProcessingOptions {
- return po.defaultOptions.clone()
- }
- // clone clones ProcessingOptions struct and its slices and maps
- func (po *ProcessingOptions) clone() *ProcessingOptions {
- clone := *po
- clone.FormatQuality = maps.Clone(po.FormatQuality)
- clone.SkipProcessingFormats = slices.Clone(po.SkipProcessingFormats)
- clone.UsedPresets = slices.Clone(po.UsedPresets)
- if po.Expires != nil {
- poExipres := *po.Expires
- clone.Expires = &poExipres
- }
- // Copy the pointer to the default options struct from parent.
- // Nil means that we have just cloned the default options struct itself
- // so we set it as default options.
- if clone.defaultOptions == nil {
- clone.defaultOptions = po
- }
- return &clone
- }
- func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...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 "zoom", "z":
- return applyZoomOption(po, args)
- case "dpr":
- return applyDprOption(po, args)
- case "enlarge", "el":
- return applyEnlargeOption(po, args)
- case "extend", "ex":
- return applyExtendOption(po, args)
- case "extend_aspect_ratio", "extend_ar", "exar":
- return applyExtendAspectRatioOption(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 "pixelate", "pix":
- return applyPixelateOption(po, args)
- case "watermark", "wm":
- return applyWatermarkOption(po, args)
- case "strip_metadata", "sm":
- return applyStripMetadataOption(po, args)
- case "keep_copyright", "kcr":
- return applyKeepCopyrightOption(po, args)
- case "strip_color_profile", "scp":
- return applyStripColorProfileOption(po, args)
- case "enforce_thumbnail", "eth":
- return applyEnforceThumbnailOption(po, args)
- // Saving options
- case "quality", "q":
- return applyQualityOption(po, args)
- case "format_quality", "fq":
- return applyFormatQualityOption(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 "raw":
- return applyRawOption(po, args)
- case "cachebuster", "cb":
- return applyCacheBusterOption(po, args)
- case "expires", "exp":
- return applyExpiresOption(po, args)
- case "filename", "fn":
- return applyFilenameOption(po, args)
- case "return_attachment", "att":
- return applyReturnAttachmentOption(po, args)
- // Presets
- case "preset", "pr":
- return applyPresetOption(f, po, args, usedPresets...)
- // Security
- case "max_src_resolution", "msr":
- return applyMaxSrcResolutionOption(po, args)
- case "max_src_file_size", "msfs":
- return applyMaxSrcFileSizeOption(po, args)
- case "max_animation_frames", "maf":
- return applyMaxAnimationFramesOption(po, args)
- case "max_animation_frame_resolution", "mafr":
- return applyMaxAnimationFrameResolutionOption(po, args)
- case "max_result_dimension", "mrd":
- return applyMaxResultDimensionOption(po, args)
- }
- return newUnknownOptionError("processing", name)
- }
- func (f *Factory) applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
- allowAll = allowAll || len(f.config.AllowedProcessingOptions) == 0
- for _, opt := range options {
- if !allowAll && !slices.Contains(f.config.AllowedProcessingOptions, opt.Name) {
- return newForbiddenOptionError("processing", opt.Name)
- }
- if err := f.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
- return err
- }
- }
- return nil
- }
- func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
- po := f.NewProcessingOptions()
- headerAccept := headers.Get("Accept")
- if strings.Contains(headerAccept, "image/webp") {
- po.PreferWebP = f.config.AutoWebp || f.config.EnforceWebp
- po.EnforceWebP = f.config.EnforceWebp
- }
- if strings.Contains(headerAccept, "image/avif") {
- po.PreferAvif = f.config.AutoAvif || f.config.EnforceAvif
- po.EnforceAvif = f.config.EnforceAvif
- }
- if strings.Contains(headerAccept, "image/jxl") {
- po.PreferJxl = f.config.AutoJxl || f.config.EnforceJxl
- po.EnforceJxl = f.config.EnforceJxl
- }
- if f.config.EnableClientHints {
- headerDPR := headers.Get("Sec-CH-DPR")
- if len(headerDPR) == 0 {
- headerDPR = headers.Get("DPR")
- }
- if len(headerDPR) > 0 {
- if dpr, err := strconv.ParseFloat(headerDPR, 64); err == nil && (dpr > 0 && dpr <= maxClientHintDPR) {
- po.Dpr = dpr
- }
- }
- headerWidth := headers.Get("Sec-CH-Width")
- if len(headerWidth) == 0 {
- headerWidth = headers.Get("Width")
- }
- if len(headerWidth) > 0 {
- if w, err := strconv.Atoi(headerWidth); err == nil {
- po.Width = imath.Shrink(w, po.Dpr)
- }
- }
- }
- if _, ok := f.presets["default"]; ok {
- if err := applyPresetOption(f, po, []string{"default"}); err != nil {
- return po, err
- }
- }
- return po, nil
- }
- // ParsePath parses the given request path and returns the processing options and image URL
- func (f *Factory) ParsePath(
- path string,
- headers http.Header,
- ) (po *ProcessingOptions, imageURL string, err error) {
- if path == "" || path == "/" {
- return nil, "", newInvalidURLError("invalid path: %s", path)
- }
- parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
- if f.config.OnlyPresets {
- po, imageURL, err = f.parsePathPresets(parts, headers)
- } else {
- po, imageURL, err = f.parsePathOptions(parts, headers)
- }
- if err != nil {
- return nil, "", ierrors.Wrap(err, 0)
- }
- return po, imageURL, nil
- }
- // parsePathOptions parses processing options from the URL path
- func (f *Factory) parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
- if _, ok := resizeTypes[parts[0]]; ok {
- return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
- }
- po, err := f.defaultProcessingOptions(headers)
- if err != nil {
- return nil, "", err
- }
- options, urlParts := f.parseURLOptions(parts)
- if err = f.applyURLOptions(po, options, false); err != nil {
- return nil, "", err
- }
- url, extension, err := f.DecodeURL(urlParts)
- if err != nil {
- return nil, "", err
- }
- if !po.Raw && len(extension) > 0 {
- if err = applyFormatOption(po, []string{extension}); err != nil {
- return nil, "", err
- }
- }
- return po, url, nil
- }
- // parsePathPresets parses presets from the URL path
- func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
- po, err := f.defaultProcessingOptions(headers)
- if err != nil {
- return nil, "", err
- }
- presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
- urlParts := parts[1:]
- if err = applyPresetOption(f, po, presets); err != nil {
- return nil, "", err
- }
- url, extension, err := f.DecodeURL(urlParts)
- if err != nil {
- return nil, "", err
- }
- if !po.Raw && len(extension) > 0 {
- if err = applyFormatOption(po, []string{extension}); err != nil {
- return nil, "", err
- }
- }
- return po, url, nil
- }
- func (po *ProcessingOptions) isSecurityOptionsAllowed() error {
- if po.config.AllowSecurityOptions {
- return nil
- }
- return newSecurityOptionsError()
- }
|