processing_options.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. package options
  2. import (
  3. "maps"
  4. "net/http"
  5. "slices"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/imgproxy/imgproxy/v3/ierrors"
  10. "github.com/imgproxy/imgproxy/v3/imagetype"
  11. "github.com/imgproxy/imgproxy/v3/imath"
  12. "github.com/imgproxy/imgproxy/v3/security"
  13. "github.com/imgproxy/imgproxy/v3/structdiff"
  14. "github.com/imgproxy/imgproxy/v3/vips"
  15. )
  16. const maxClientHintDPR = 8
  17. type ExtendOptions struct {
  18. Enabled bool
  19. Gravity GravityOptions
  20. }
  21. type CropOptions struct {
  22. Width float64
  23. Height float64
  24. Gravity GravityOptions
  25. }
  26. type PaddingOptions struct {
  27. Enabled bool
  28. Top int
  29. Right int
  30. Bottom int
  31. Left int
  32. }
  33. type TrimOptions struct {
  34. Enabled bool
  35. Threshold float64
  36. Smart bool
  37. Color vips.Color
  38. EqualHor bool
  39. EqualVer bool
  40. }
  41. type WatermarkOptions struct {
  42. Enabled bool
  43. Opacity float64
  44. Position GravityOptions
  45. Scale float64
  46. }
  47. func (wo WatermarkOptions) ShouldReplicate() bool {
  48. return wo.Position.Type == GravityReplicate
  49. }
  50. type ProcessingOptions struct {
  51. defaultOptions *ProcessingOptions
  52. config *Config
  53. ResizingType ResizeType
  54. Width int
  55. Height int
  56. MinWidth int
  57. MinHeight int
  58. ZoomWidth float64
  59. ZoomHeight float64
  60. Dpr float64
  61. Gravity GravityOptions
  62. Enlarge bool
  63. Extend ExtendOptions
  64. ExtendAspectRatio ExtendOptions
  65. Crop CropOptions
  66. Padding PaddingOptions
  67. Trim TrimOptions
  68. Rotate int
  69. Format imagetype.Type
  70. Quality int
  71. FormatQuality map[imagetype.Type]int
  72. MaxBytes int
  73. Flatten bool
  74. Background vips.Color
  75. Blur float32
  76. Sharpen float32
  77. Pixelate int
  78. StripMetadata bool
  79. KeepCopyright bool
  80. StripColorProfile bool
  81. AutoRotate bool
  82. EnforceThumbnail bool
  83. SkipProcessingFormats []imagetype.Type
  84. CacheBuster string
  85. Expires *time.Time
  86. Watermark WatermarkOptions
  87. PreferWebP bool
  88. EnforceWebP bool
  89. PreferAvif bool
  90. EnforceAvif bool
  91. PreferJxl bool
  92. EnforceJxl bool
  93. Filename string
  94. ReturnAttachment bool
  95. Raw bool
  96. UsedPresets []string
  97. SecurityOptions security.Options
  98. }
  99. func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
  100. po := ProcessingOptions{
  101. config: config,
  102. ResizingType: ResizeFit,
  103. Width: 0,
  104. Height: 0,
  105. ZoomWidth: 1,
  106. ZoomHeight: 1,
  107. Gravity: GravityOptions{Type: GravityCenter},
  108. Enlarge: false,
  109. Extend: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
  110. ExtendAspectRatio: ExtendOptions{Enabled: false, Gravity: GravityOptions{Type: GravityCenter}},
  111. Padding: PaddingOptions{Enabled: false},
  112. Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
  113. Rotate: 0,
  114. Quality: 0,
  115. FormatQuality: maps.Clone(config.FormatQuality),
  116. MaxBytes: 0,
  117. Format: imagetype.Unknown,
  118. Background: vips.Color{R: 255, G: 255, B: 255},
  119. Blur: 0,
  120. Sharpen: 0,
  121. Dpr: 1,
  122. Watermark: WatermarkOptions{Opacity: 1, Position: GravityOptions{Type: GravityCenter}},
  123. StripMetadata: config.StripMetadata,
  124. KeepCopyright: config.KeepCopyright,
  125. StripColorProfile: config.StripColorProfile,
  126. AutoRotate: config.AutoRotate,
  127. EnforceThumbnail: config.EnforceThumbnail,
  128. ReturnAttachment: config.ReturnAttachment,
  129. SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
  130. SecurityOptions: security.NewOptions(),
  131. }
  132. return &po
  133. }
  134. func (po *ProcessingOptions) GetQuality() int {
  135. q := po.Quality
  136. if q == 0 {
  137. q = po.FormatQuality[po.Format]
  138. }
  139. if q == 0 {
  140. q = po.config.Quality
  141. }
  142. return q
  143. }
  144. func (po *ProcessingOptions) Diff() structdiff.Entries {
  145. return structdiff.Diff(po.defaultOptions, po)
  146. }
  147. func (po *ProcessingOptions) String() string {
  148. return po.Diff().String()
  149. }
  150. func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
  151. return po.Diff().MarshalJSON()
  152. }
  153. // Default returns the ProcessingOptions instance with defaults set
  154. func (po *ProcessingOptions) Default() *ProcessingOptions {
  155. return po.defaultOptions.clone()
  156. }
  157. // clone clones ProcessingOptions struct and its slices and maps
  158. func (po *ProcessingOptions) clone() *ProcessingOptions {
  159. clone := *po
  160. clone.FormatQuality = maps.Clone(po.FormatQuality)
  161. clone.SkipProcessingFormats = slices.Clone(po.SkipProcessingFormats)
  162. clone.UsedPresets = slices.Clone(po.UsedPresets)
  163. if po.Expires != nil {
  164. poExipres := *po.Expires
  165. clone.Expires = &poExipres
  166. }
  167. // Copy the pointer to the default options struct from parent.
  168. // Nil means that we have just cloned the default options struct itself
  169. // so we set it as default options.
  170. if clone.defaultOptions == nil {
  171. clone.defaultOptions = po
  172. }
  173. return &clone
  174. }
  175. func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
  176. switch name {
  177. case "resize", "rs":
  178. return applyResizeOption(po, args)
  179. case "size", "s":
  180. return applySizeOption(po, args)
  181. case "resizing_type", "rt":
  182. return applyResizingTypeOption(po, args)
  183. case "width", "w":
  184. return applyWidthOption(po, args)
  185. case "height", "h":
  186. return applyHeightOption(po, args)
  187. case "min-width", "mw":
  188. return applyMinWidthOption(po, args)
  189. case "min-height", "mh":
  190. return applyMinHeightOption(po, args)
  191. case "zoom", "z":
  192. return applyZoomOption(po, args)
  193. case "dpr":
  194. return applyDprOption(po, args)
  195. case "enlarge", "el":
  196. return applyEnlargeOption(po, args)
  197. case "extend", "ex":
  198. return applyExtendOption(po, args)
  199. case "extend_aspect_ratio", "extend_ar", "exar":
  200. return applyExtendAspectRatioOption(po, args)
  201. case "gravity", "g":
  202. return applyGravityOption(po, args)
  203. case "crop", "c":
  204. return applyCropOption(po, args)
  205. case "trim", "t":
  206. return applyTrimOption(po, args)
  207. case "padding", "pd":
  208. return applyPaddingOption(po, args)
  209. case "auto_rotate", "ar":
  210. return applyAutoRotateOption(po, args)
  211. case "rotate", "rot":
  212. return applyRotateOption(po, args)
  213. case "background", "bg":
  214. return applyBackgroundOption(po, args)
  215. case "blur", "bl":
  216. return applyBlurOption(po, args)
  217. case "sharpen", "sh":
  218. return applySharpenOption(po, args)
  219. case "pixelate", "pix":
  220. return applyPixelateOption(po, args)
  221. case "watermark", "wm":
  222. return applyWatermarkOption(po, args)
  223. case "strip_metadata", "sm":
  224. return applyStripMetadataOption(po, args)
  225. case "keep_copyright", "kcr":
  226. return applyKeepCopyrightOption(po, args)
  227. case "strip_color_profile", "scp":
  228. return applyStripColorProfileOption(po, args)
  229. case "enforce_thumbnail", "eth":
  230. return applyEnforceThumbnailOption(po, args)
  231. // Saving options
  232. case "quality", "q":
  233. return applyQualityOption(po, args)
  234. case "format_quality", "fq":
  235. return applyFormatQualityOption(po, args)
  236. case "max_bytes", "mb":
  237. return applyMaxBytesOption(po, args)
  238. case "format", "f", "ext":
  239. return applyFormatOption(po, args)
  240. // Handling options
  241. case "skip_processing", "skp":
  242. return applySkipProcessingFormatsOption(po, args)
  243. case "raw":
  244. return applyRawOption(po, args)
  245. case "cachebuster", "cb":
  246. return applyCacheBusterOption(po, args)
  247. case "expires", "exp":
  248. return applyExpiresOption(po, args)
  249. case "filename", "fn":
  250. return applyFilenameOption(po, args)
  251. case "return_attachment", "att":
  252. return applyReturnAttachmentOption(po, args)
  253. // Presets
  254. case "preset", "pr":
  255. return applyPresetOption(f, po, args, usedPresets...)
  256. // Security
  257. case "max_src_resolution", "msr":
  258. return applyMaxSrcResolutionOption(po, args)
  259. case "max_src_file_size", "msfs":
  260. return applyMaxSrcFileSizeOption(po, args)
  261. case "max_animation_frames", "maf":
  262. return applyMaxAnimationFramesOption(po, args)
  263. case "max_animation_frame_resolution", "mafr":
  264. return applyMaxAnimationFrameResolutionOption(po, args)
  265. case "max_result_dimension", "mrd":
  266. return applyMaxResultDimensionOption(po, args)
  267. }
  268. return newUnknownOptionError("processing", name)
  269. }
  270. func (f *Factory) applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
  271. allowAll = allowAll || len(f.config.AllowedProcessingOptions) == 0
  272. for _, opt := range options {
  273. if !allowAll && !slices.Contains(f.config.AllowedProcessingOptions, opt.Name) {
  274. return newForbiddenOptionError("processing", opt.Name)
  275. }
  276. if err := f.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
  277. return err
  278. }
  279. }
  280. return nil
  281. }
  282. func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
  283. po := f.NewProcessingOptions()
  284. headerAccept := headers.Get("Accept")
  285. if strings.Contains(headerAccept, "image/webp") {
  286. po.PreferWebP = f.config.AutoWebp || f.config.EnforceWebp
  287. po.EnforceWebP = f.config.EnforceWebp
  288. }
  289. if strings.Contains(headerAccept, "image/avif") {
  290. po.PreferAvif = f.config.AutoAvif || f.config.EnforceAvif
  291. po.EnforceAvif = f.config.EnforceAvif
  292. }
  293. if strings.Contains(headerAccept, "image/jxl") {
  294. po.PreferJxl = f.config.AutoJxl || f.config.EnforceJxl
  295. po.EnforceJxl = f.config.EnforceJxl
  296. }
  297. if f.config.EnableClientHints {
  298. headerDPR := headers.Get("Sec-CH-DPR")
  299. if len(headerDPR) == 0 {
  300. headerDPR = headers.Get("DPR")
  301. }
  302. if len(headerDPR) > 0 {
  303. if dpr, err := strconv.ParseFloat(headerDPR, 64); err == nil && (dpr > 0 && dpr <= maxClientHintDPR) {
  304. po.Dpr = dpr
  305. }
  306. }
  307. headerWidth := headers.Get("Sec-CH-Width")
  308. if len(headerWidth) == 0 {
  309. headerWidth = headers.Get("Width")
  310. }
  311. if len(headerWidth) > 0 {
  312. if w, err := strconv.Atoi(headerWidth); err == nil {
  313. po.Width = imath.Shrink(w, po.Dpr)
  314. }
  315. }
  316. }
  317. if _, ok := f.presets["default"]; ok {
  318. if err := applyPresetOption(f, po, []string{"default"}); err != nil {
  319. return po, err
  320. }
  321. }
  322. return po, nil
  323. }
  324. // ParsePath parses the given request path and returns the processing options and image URL
  325. func (f *Factory) ParsePath(
  326. path string,
  327. headers http.Header,
  328. ) (po *ProcessingOptions, imageURL string, err error) {
  329. if path == "" || path == "/" {
  330. return nil, "", newInvalidURLError("invalid path: %s", path)
  331. }
  332. parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
  333. if f.config.OnlyPresets {
  334. po, imageURL, err = f.parsePathPresets(parts, headers)
  335. } else {
  336. po, imageURL, err = f.parsePathOptions(parts, headers)
  337. }
  338. if err != nil {
  339. return nil, "", ierrors.Wrap(err, 0)
  340. }
  341. return po, imageURL, nil
  342. }
  343. // parsePathOptions parses processing options from the URL path
  344. func (f *Factory) parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
  345. if _, ok := resizeTypes[parts[0]]; ok {
  346. return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
  347. }
  348. po, err := f.defaultProcessingOptions(headers)
  349. if err != nil {
  350. return nil, "", err
  351. }
  352. options, urlParts := f.parseURLOptions(parts)
  353. if err = f.applyURLOptions(po, options, false); err != nil {
  354. return nil, "", err
  355. }
  356. url, extension, err := f.DecodeURL(urlParts)
  357. if err != nil {
  358. return nil, "", err
  359. }
  360. if !po.Raw && len(extension) > 0 {
  361. if err = applyFormatOption(po, []string{extension}); err != nil {
  362. return nil, "", err
  363. }
  364. }
  365. return po, url, nil
  366. }
  367. // parsePathPresets parses presets from the URL path
  368. func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
  369. po, err := f.defaultProcessingOptions(headers)
  370. if err != nil {
  371. return nil, "", err
  372. }
  373. presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
  374. urlParts := parts[1:]
  375. if err = applyPresetOption(f, po, presets); err != nil {
  376. return nil, "", err
  377. }
  378. url, extension, err := f.DecodeURL(urlParts)
  379. if err != nil {
  380. return nil, "", err
  381. }
  382. if !po.Raw && len(extension) > 0 {
  383. if err = applyFormatOption(po, []string{extension}); err != nil {
  384. return nil, "", err
  385. }
  386. }
  387. return po, url, nil
  388. }
  389. func (po *ProcessingOptions) isSecurityOptionsAllowed() error {
  390. if po.config.AllowSecurityOptions {
  391. return nil
  392. }
  393. return newSecurityOptionsError()
  394. }