processing_options.go 12 KB

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