processing_options.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. package options
  2. import (
  3. "encoding/base64"
  4. "net/http"
  5. "slices"
  6. "strconv"
  7. "strings"
  8. "time"
  9. log "github.com/sirupsen/logrus"
  10. "github.com/imgproxy/imgproxy/v3/config"
  11. "github.com/imgproxy/imgproxy/v3/ierrors"
  12. "github.com/imgproxy/imgproxy/v3/imagetype"
  13. "github.com/imgproxy/imgproxy/v3/imath"
  14. "github.com/imgproxy/imgproxy/v3/security"
  15. "github.com/imgproxy/imgproxy/v3/structdiff"
  16. "github.com/imgproxy/imgproxy/v3/vips"
  17. )
  18. const maxClientHintDPR = 8
  19. type ExtendOptions struct {
  20. Enabled bool
  21. Gravity GravityOptions
  22. }
  23. type CropOptions struct {
  24. Width float64
  25. Height float64
  26. Gravity GravityOptions
  27. }
  28. type PaddingOptions struct {
  29. Enabled bool
  30. Top int
  31. Right int
  32. Bottom int
  33. Left int
  34. }
  35. type TrimOptions struct {
  36. Enabled bool
  37. Threshold float64
  38. Smart bool
  39. Color vips.Color
  40. EqualHor bool
  41. EqualVer bool
  42. }
  43. type WatermarkOptions struct {
  44. Enabled bool
  45. Opacity float64
  46. Position GravityOptions
  47. Scale float64
  48. }
  49. func (wo WatermarkOptions) ShouldReplicate() bool {
  50. return wo.Position.Type == GravityReplicate
  51. }
  52. type ProcessingOptions struct {
  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. defaultQuality int
  99. }
  100. func NewProcessingOptions() *ProcessingOptions {
  101. po := ProcessingOptions{
  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. MaxBytes: 0,
  116. Format: imagetype.Unknown,
  117. Background: vips.Color{R: 255, G: 255, B: 255},
  118. Blur: 0,
  119. Sharpen: 0,
  120. Dpr: 1,
  121. Watermark: WatermarkOptions{Opacity: 1, Position: GravityOptions{Type: GravityCenter}},
  122. StripMetadata: config.StripMetadata,
  123. KeepCopyright: config.KeepCopyright,
  124. StripColorProfile: config.StripColorProfile,
  125. AutoRotate: config.AutoRotate,
  126. EnforceThumbnail: config.EnforceThumbnail,
  127. ReturnAttachment: config.ReturnAttachment,
  128. SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
  129. UsedPresets: make([]string, 0, len(config.Presets)),
  130. SecurityOptions: security.DefaultOptions(),
  131. // Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
  132. defaultQuality: config.Quality,
  133. }
  134. po.FormatQuality = make(map[imagetype.Type]int, len(config.FormatQuality))
  135. for k, v := range config.FormatQuality {
  136. po.FormatQuality[k] = v
  137. }
  138. return &po
  139. }
  140. func (po *ProcessingOptions) GetQuality() int {
  141. q := po.Quality
  142. if q == 0 {
  143. q = po.FormatQuality[po.Format]
  144. }
  145. if q == 0 {
  146. q = po.defaultQuality
  147. }
  148. return q
  149. }
  150. func (po *ProcessingOptions) Diff() structdiff.Entries {
  151. return structdiff.Diff(NewProcessingOptions(), po)
  152. }
  153. func (po *ProcessingOptions) String() string {
  154. return po.Diff().String()
  155. }
  156. func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
  157. return po.Diff().MarshalJSON()
  158. }
  159. func parseDimension(d *int, name, arg string) error {
  160. if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
  161. *d = v
  162. } else {
  163. return newOptionArgumentError("Invalid %s: %s", name, arg)
  164. }
  165. return nil
  166. }
  167. func parseBoolOption(str string) bool {
  168. b, err := strconv.ParseBool(str)
  169. if err != nil {
  170. log.Warningf("`%s` is not a valid boolean value. Treated as false", str)
  171. }
  172. return b
  173. }
  174. func isGravityOffcetValid(gravity GravityType, offset float64) bool {
  175. return gravity != GravityFocusPoint || (offset >= 0 && offset <= 1)
  176. }
  177. func parseGravity(g *GravityOptions, name string, args []string, allowedTypes []GravityType) error {
  178. nArgs := len(args)
  179. if t, ok := gravityTypes[args[0]]; ok && slices.Contains(allowedTypes, t) {
  180. g.Type = t
  181. } else {
  182. return newOptionArgumentError("Invalid %s: %s", name, args[0])
  183. }
  184. switch g.Type {
  185. case GravitySmart:
  186. if nArgs > 1 {
  187. return newOptionArgumentError("Invalid %s arguments: %v", name, args)
  188. }
  189. g.X, g.Y = 0.0, 0.0
  190. case GravityFocusPoint:
  191. if nArgs != 3 {
  192. return newOptionArgumentError("Invalid %s arguments: %v", name, args)
  193. }
  194. fallthrough
  195. default:
  196. if nArgs > 3 {
  197. return newOptionArgumentError("Invalid %s arguments: %v", name, args)
  198. }
  199. if nArgs > 1 {
  200. if x, err := strconv.ParseFloat(args[1], 64); err == nil && isGravityOffcetValid(g.Type, x) {
  201. g.X = x
  202. } else {
  203. return newOptionArgumentError("Invalid %s X: %s", name, args[1])
  204. }
  205. }
  206. if nArgs > 2 {
  207. if y, err := strconv.ParseFloat(args[2], 64); err == nil && isGravityOffcetValid(g.Type, y) {
  208. g.Y = y
  209. } else {
  210. return newOptionArgumentError("Invalid %s Y: %s", name, args[2])
  211. }
  212. }
  213. }
  214. return nil
  215. }
  216. func parseExtend(opts *ExtendOptions, name string, args []string) error {
  217. if len(args) > 4 {
  218. return newOptionArgumentError("Invalid %s arguments: %v", name, args)
  219. }
  220. opts.Enabled = parseBoolOption(args[0])
  221. if len(args) > 1 {
  222. return parseGravity(&opts.Gravity, name+" gravity", args[1:], extendGravityTypes)
  223. }
  224. return nil
  225. }
  226. func applyWidthOption(po *ProcessingOptions, args []string) error {
  227. if len(args) > 1 {
  228. return newOptionArgumentError("Invalid width arguments: %v", args)
  229. }
  230. return parseDimension(&po.Width, "width", args[0])
  231. }
  232. func applyHeightOption(po *ProcessingOptions, args []string) error {
  233. if len(args) > 1 {
  234. return newOptionArgumentError("Invalid height arguments: %v", args)
  235. }
  236. return parseDimension(&po.Height, "height", args[0])
  237. }
  238. func applyMinWidthOption(po *ProcessingOptions, args []string) error {
  239. if len(args) > 1 {
  240. return newOptionArgumentError("Invalid min width arguments: %v", args)
  241. }
  242. return parseDimension(&po.MinWidth, "min width", args[0])
  243. }
  244. func applyMinHeightOption(po *ProcessingOptions, args []string) error {
  245. if len(args) > 1 {
  246. return newOptionArgumentError("Invalid min height arguments: %v", args)
  247. }
  248. return parseDimension(&po.MinHeight, " min height", args[0])
  249. }
  250. func applyEnlargeOption(po *ProcessingOptions, args []string) error {
  251. if len(args) > 1 {
  252. return newOptionArgumentError("Invalid enlarge arguments: %v", args)
  253. }
  254. po.Enlarge = parseBoolOption(args[0])
  255. return nil
  256. }
  257. func applyExtendOption(po *ProcessingOptions, args []string) error {
  258. return parseExtend(&po.Extend, "extend", args)
  259. }
  260. func applyExtendAspectRatioOption(po *ProcessingOptions, args []string) error {
  261. return parseExtend(&po.ExtendAspectRatio, "extend_aspect_ratio", args)
  262. }
  263. func applySizeOption(po *ProcessingOptions, args []string) (err error) {
  264. if len(args) > 7 {
  265. return newOptionArgumentError("Invalid size arguments: %v", args)
  266. }
  267. if len(args) >= 1 && len(args[0]) > 0 {
  268. if err = applyWidthOption(po, args[0:1]); err != nil {
  269. return
  270. }
  271. }
  272. if len(args) >= 2 && len(args[1]) > 0 {
  273. if err = applyHeightOption(po, args[1:2]); err != nil {
  274. return
  275. }
  276. }
  277. if len(args) >= 3 && len(args[2]) > 0 {
  278. if err = applyEnlargeOption(po, args[2:3]); err != nil {
  279. return
  280. }
  281. }
  282. if len(args) >= 4 && len(args[3]) > 0 {
  283. if err = applyExtendOption(po, args[3:]); err != nil {
  284. return
  285. }
  286. }
  287. return nil
  288. }
  289. func applyResizingTypeOption(po *ProcessingOptions, args []string) error {
  290. if len(args) > 1 {
  291. return newOptionArgumentError("Invalid resizing type arguments: %v", args)
  292. }
  293. if r, ok := resizeTypes[args[0]]; ok {
  294. po.ResizingType = r
  295. } else {
  296. return newOptionArgumentError("Invalid resize type: %s", args[0])
  297. }
  298. return nil
  299. }
  300. func applyResizeOption(po *ProcessingOptions, args []string) error {
  301. if len(args) > 8 {
  302. return newOptionArgumentError("Invalid resize arguments: %v", args)
  303. }
  304. if len(args[0]) > 0 {
  305. if err := applyResizingTypeOption(po, args[0:1]); err != nil {
  306. return err
  307. }
  308. }
  309. if len(args) > 1 {
  310. if err := applySizeOption(po, args[1:]); err != nil {
  311. return err
  312. }
  313. }
  314. return nil
  315. }
  316. func applyZoomOption(po *ProcessingOptions, args []string) error {
  317. nArgs := len(args)
  318. if nArgs > 2 {
  319. return newOptionArgumentError("Invalid zoom arguments: %v", args)
  320. }
  321. if z, err := strconv.ParseFloat(args[0], 64); err == nil && z > 0 {
  322. po.ZoomWidth = z
  323. po.ZoomHeight = z
  324. } else {
  325. return newOptionArgumentError("Invalid zoom value: %s", args[0])
  326. }
  327. if nArgs > 1 {
  328. if z, err := strconv.ParseFloat(args[1], 64); err == nil && z > 0 {
  329. po.ZoomHeight = z
  330. } else {
  331. return newOptionArgumentError("Invalid zoom value: %s", args[1])
  332. }
  333. }
  334. return nil
  335. }
  336. func applyDprOption(po *ProcessingOptions, args []string) error {
  337. if len(args) > 1 {
  338. return newOptionArgumentError("Invalid dpr arguments: %v", args)
  339. }
  340. if d, err := strconv.ParseFloat(args[0], 64); err == nil && d > 0 {
  341. po.Dpr = d
  342. } else {
  343. return newOptionArgumentError("Invalid dpr: %s", args[0])
  344. }
  345. return nil
  346. }
  347. func applyGravityOption(po *ProcessingOptions, args []string) error {
  348. return parseGravity(&po.Gravity, "gravity", args, cropGravityTypes)
  349. }
  350. func applyCropOption(po *ProcessingOptions, args []string) error {
  351. if w, err := strconv.ParseFloat(args[0], 64); err == nil && w >= 0 {
  352. po.Crop.Width = w
  353. } else {
  354. return newOptionArgumentError("Invalid crop width: %s", args[0])
  355. }
  356. if len(args) > 1 {
  357. if h, err := strconv.ParseFloat(args[1], 64); err == nil && h >= 0 {
  358. po.Crop.Height = h
  359. } else {
  360. return newOptionArgumentError("Invalid crop height: %s", args[1])
  361. }
  362. }
  363. if len(args) > 2 {
  364. return parseGravity(&po.Crop.Gravity, "crop gravity", args[2:], cropGravityTypes)
  365. }
  366. return nil
  367. }
  368. func applyPaddingOption(po *ProcessingOptions, args []string) error {
  369. nArgs := len(args)
  370. if nArgs < 1 || nArgs > 4 {
  371. return newOptionArgumentError("Invalid padding arguments: %v", args)
  372. }
  373. po.Padding.Enabled = true
  374. if nArgs > 0 && len(args[0]) > 0 {
  375. if err := parseDimension(&po.Padding.Top, "padding top (+all)", args[0]); err != nil {
  376. return err
  377. }
  378. po.Padding.Right = po.Padding.Top
  379. po.Padding.Bottom = po.Padding.Top
  380. po.Padding.Left = po.Padding.Top
  381. }
  382. if nArgs > 1 && len(args[1]) > 0 {
  383. if err := parseDimension(&po.Padding.Right, "padding right (+left)", args[1]); err != nil {
  384. return err
  385. }
  386. po.Padding.Left = po.Padding.Right
  387. }
  388. if nArgs > 2 && len(args[2]) > 0 {
  389. if err := parseDimension(&po.Padding.Bottom, "padding bottom", args[2]); err != nil {
  390. return err
  391. }
  392. }
  393. if nArgs > 3 && len(args[3]) > 0 {
  394. if err := parseDimension(&po.Padding.Left, "padding left", args[3]); err != nil {
  395. return err
  396. }
  397. }
  398. if po.Padding.Top == 0 && po.Padding.Right == 0 && po.Padding.Bottom == 0 && po.Padding.Left == 0 {
  399. po.Padding.Enabled = false
  400. }
  401. return nil
  402. }
  403. func applyTrimOption(po *ProcessingOptions, args []string) error {
  404. nArgs := len(args)
  405. if nArgs > 4 {
  406. return newOptionArgumentError("Invalid trim arguments: %v", args)
  407. }
  408. if t, err := strconv.ParseFloat(args[0], 64); err == nil && t >= 0 {
  409. po.Trim.Enabled = true
  410. po.Trim.Threshold = t
  411. } else {
  412. return newOptionArgumentError("Invalid trim threshold: %s", args[0])
  413. }
  414. if nArgs > 1 && len(args[1]) > 0 {
  415. if c, err := vips.ColorFromHex(args[1]); err == nil {
  416. po.Trim.Color = c
  417. po.Trim.Smart = false
  418. } else {
  419. return newOptionArgumentError("Invalid trim color: %s", args[1])
  420. }
  421. }
  422. if nArgs > 2 && len(args[2]) > 0 {
  423. po.Trim.EqualHor = parseBoolOption(args[2])
  424. }
  425. if nArgs > 3 && len(args[3]) > 0 {
  426. po.Trim.EqualVer = parseBoolOption(args[3])
  427. }
  428. return nil
  429. }
  430. func applyRotateOption(po *ProcessingOptions, args []string) error {
  431. if len(args) > 1 {
  432. return newOptionArgumentError("Invalid rotate arguments: %v", args)
  433. }
  434. if r, err := strconv.Atoi(args[0]); err == nil && r%90 == 0 {
  435. po.Rotate = r
  436. } else {
  437. return newOptionArgumentError("Invalid rotation angle: %s", args[0])
  438. }
  439. return nil
  440. }
  441. func applyQualityOption(po *ProcessingOptions, args []string) error {
  442. if len(args) > 1 {
  443. return newOptionArgumentError("Invalid quality arguments: %v", args)
  444. }
  445. if q, err := strconv.Atoi(args[0]); err == nil && q >= 0 && q <= 100 {
  446. po.Quality = q
  447. } else {
  448. return newOptionArgumentError("Invalid quality: %s", args[0])
  449. }
  450. return nil
  451. }
  452. func applyFormatQualityOption(po *ProcessingOptions, args []string) error {
  453. argsLen := len(args)
  454. if len(args)%2 != 0 {
  455. return newOptionArgumentError("Missing quality for: %s", args[argsLen-1])
  456. }
  457. for i := 0; i < argsLen; i += 2 {
  458. f, ok := imagetype.Types[args[i]]
  459. if !ok {
  460. return newOptionArgumentError("Invalid image format: %s", args[i])
  461. }
  462. if q, err := strconv.Atoi(args[i+1]); err == nil && q >= 0 && q <= 100 {
  463. po.FormatQuality[f] = q
  464. } else {
  465. return newOptionArgumentError("Invalid quality for %s: %s", args[i], args[i+1])
  466. }
  467. }
  468. return nil
  469. }
  470. func applyMaxBytesOption(po *ProcessingOptions, args []string) error {
  471. if len(args) > 1 {
  472. return newOptionArgumentError("Invalid max_bytes arguments: %v", args)
  473. }
  474. if max, err := strconv.Atoi(args[0]); err == nil && max >= 0 {
  475. po.MaxBytes = max
  476. } else {
  477. return newOptionArgumentError("Invalid max_bytes: %s", args[0])
  478. }
  479. return nil
  480. }
  481. func applyBackgroundOption(po *ProcessingOptions, args []string) error {
  482. switch len(args) {
  483. case 1:
  484. if len(args[0]) == 0 {
  485. po.Flatten = false
  486. } else if c, err := vips.ColorFromHex(args[0]); err == nil {
  487. po.Flatten = true
  488. po.Background = c
  489. } else {
  490. return newOptionArgumentError("Invalid background argument: %s", err)
  491. }
  492. case 3:
  493. po.Flatten = true
  494. if r, err := strconv.ParseUint(args[0], 10, 8); err == nil && r <= 255 {
  495. po.Background.R = uint8(r)
  496. } else {
  497. return newOptionArgumentError("Invalid background red channel: %s", args[0])
  498. }
  499. if g, err := strconv.ParseUint(args[1], 10, 8); err == nil && g <= 255 {
  500. po.Background.G = uint8(g)
  501. } else {
  502. return newOptionArgumentError("Invalid background green channel: %s", args[1])
  503. }
  504. if b, err := strconv.ParseUint(args[2], 10, 8); err == nil && b <= 255 {
  505. po.Background.B = uint8(b)
  506. } else {
  507. return newOptionArgumentError("Invalid background blue channel: %s", args[2])
  508. }
  509. default:
  510. return newOptionArgumentError("Invalid background arguments: %v", args)
  511. }
  512. return nil
  513. }
  514. func applyBlurOption(po *ProcessingOptions, args []string) error {
  515. if len(args) > 1 {
  516. return newOptionArgumentError("Invalid blur arguments: %v", args)
  517. }
  518. if b, err := strconv.ParseFloat(args[0], 32); err == nil && b >= 0 {
  519. po.Blur = float32(b)
  520. } else {
  521. return newOptionArgumentError("Invalid blur: %s", args[0])
  522. }
  523. return nil
  524. }
  525. func applySharpenOption(po *ProcessingOptions, args []string) error {
  526. if len(args) > 1 {
  527. return newOptionArgumentError("Invalid sharpen arguments: %v", args)
  528. }
  529. if s, err := strconv.ParseFloat(args[0], 32); err == nil && s >= 0 {
  530. po.Sharpen = float32(s)
  531. } else {
  532. return newOptionArgumentError("Invalid sharpen: %s", args[0])
  533. }
  534. return nil
  535. }
  536. func applyPixelateOption(po *ProcessingOptions, args []string) error {
  537. if len(args) > 1 {
  538. return newOptionArgumentError("Invalid pixelate arguments: %v", args)
  539. }
  540. if p, err := strconv.Atoi(args[0]); err == nil && p >= 0 {
  541. po.Pixelate = p
  542. } else {
  543. return newOptionArgumentError("Invalid pixelate: %s", args[0])
  544. }
  545. return nil
  546. }
  547. func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
  548. for _, preset := range args {
  549. if p, ok := presets[preset]; ok {
  550. if slices.Contains(usedPresets, preset) {
  551. log.Warningf("Recursive preset usage is detected: %s", preset)
  552. continue
  553. }
  554. po.UsedPresets = append(po.UsedPresets, preset)
  555. if err := applyURLOptions(po, p, append(usedPresets, preset)...); err != nil {
  556. return err
  557. }
  558. } else {
  559. return newOptionArgumentError("Unknown preset: %s", preset)
  560. }
  561. }
  562. return nil
  563. }
  564. func applyWatermarkOption(po *ProcessingOptions, args []string) error {
  565. if len(args) > 7 {
  566. return newOptionArgumentError("Invalid watermark arguments: %v", args)
  567. }
  568. if o, err := strconv.ParseFloat(args[0], 64); err == nil && o >= 0 && o <= 1 {
  569. po.Watermark.Enabled = o > 0
  570. po.Watermark.Opacity = o
  571. } else {
  572. return newOptionArgumentError("Invalid watermark opacity: %s", args[0])
  573. }
  574. if len(args) > 1 && len(args[1]) > 0 {
  575. if g, ok := gravityTypes[args[1]]; ok && slices.Contains(watermarkGravityTypes, g) {
  576. po.Watermark.Position.Type = g
  577. } else {
  578. return newOptionArgumentError("Invalid watermark position: %s", args[1])
  579. }
  580. }
  581. if len(args) > 2 && len(args[2]) > 0 {
  582. if x, err := strconv.ParseFloat(args[2], 64); err == nil {
  583. po.Watermark.Position.X = x
  584. } else {
  585. return newOptionArgumentError("Invalid watermark X offset: %s", args[2])
  586. }
  587. }
  588. if len(args) > 3 && len(args[3]) > 0 {
  589. if y, err := strconv.ParseFloat(args[3], 64); err == nil {
  590. po.Watermark.Position.Y = y
  591. } else {
  592. return newOptionArgumentError("Invalid watermark Y offset: %s", args[3])
  593. }
  594. }
  595. if len(args) > 4 && len(args[4]) > 0 {
  596. if s, err := strconv.ParseFloat(args[4], 64); err == nil && s >= 0 {
  597. po.Watermark.Scale = s
  598. } else {
  599. return newOptionArgumentError("Invalid watermark scale: %s", args[4])
  600. }
  601. }
  602. return nil
  603. }
  604. func applyFormatOption(po *ProcessingOptions, args []string) error {
  605. if len(args) > 1 {
  606. return newOptionArgumentError("Invalid format arguments: %v", args)
  607. }
  608. if f, ok := imagetype.Types[args[0]]; ok {
  609. po.Format = f
  610. } else {
  611. return newOptionArgumentError("Invalid image format: %s", args[0])
  612. }
  613. return nil
  614. }
  615. func applyCacheBusterOption(po *ProcessingOptions, args []string) error {
  616. if len(args) > 1 {
  617. return newOptionArgumentError("Invalid cache buster arguments: %v", args)
  618. }
  619. po.CacheBuster = args[0]
  620. return nil
  621. }
  622. func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) error {
  623. for _, format := range args {
  624. if f, ok := imagetype.Types[format]; ok {
  625. po.SkipProcessingFormats = append(po.SkipProcessingFormats, f)
  626. } else {
  627. return newOptionArgumentError("Invalid image format in skip processing: %s", format)
  628. }
  629. }
  630. return nil
  631. }
  632. func applyRawOption(po *ProcessingOptions, args []string) error {
  633. if len(args) > 1 {
  634. return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
  635. }
  636. po.Raw = parseBoolOption(args[0])
  637. return nil
  638. }
  639. func applyFilenameOption(po *ProcessingOptions, args []string) error {
  640. if len(args) > 2 {
  641. return newOptionArgumentError("Invalid filename arguments: %v", args)
  642. }
  643. po.Filename = args[0]
  644. if len(args) > 1 && parseBoolOption(args[1]) {
  645. decoded, err := base64.RawURLEncoding.DecodeString(po.Filename)
  646. if err != nil {
  647. return newOptionArgumentError("Invalid filename encoding: %s", err)
  648. }
  649. po.Filename = string(decoded)
  650. }
  651. return nil
  652. }
  653. func applyExpiresOption(po *ProcessingOptions, args []string) error {
  654. if len(args) > 1 {
  655. return newOptionArgumentError("Invalid expires arguments: %v", args)
  656. }
  657. timestamp, err := strconv.ParseInt(args[0], 10, 64)
  658. if err != nil {
  659. return newOptionArgumentError("Invalid expires argument: %v", args[0])
  660. }
  661. if timestamp > 0 && timestamp < time.Now().Unix() {
  662. return newOptionArgumentError("Expired URL")
  663. }
  664. expires := time.Unix(timestamp, 0)
  665. po.Expires = &expires
  666. return nil
  667. }
  668. func applyStripMetadataOption(po *ProcessingOptions, args []string) error {
  669. if len(args) > 1 {
  670. return newOptionArgumentError("Invalid strip metadata arguments: %v", args)
  671. }
  672. po.StripMetadata = parseBoolOption(args[0])
  673. return nil
  674. }
  675. func applyKeepCopyrightOption(po *ProcessingOptions, args []string) error {
  676. if len(args) > 1 {
  677. return newOptionArgumentError("Invalid keep copyright arguments: %v", args)
  678. }
  679. po.KeepCopyright = parseBoolOption(args[0])
  680. return nil
  681. }
  682. func applyStripColorProfileOption(po *ProcessingOptions, args []string) error {
  683. if len(args) > 1 {
  684. return newOptionArgumentError("Invalid strip color profile arguments: %v", args)
  685. }
  686. po.StripColorProfile = parseBoolOption(args[0])
  687. return nil
  688. }
  689. func applyAutoRotateOption(po *ProcessingOptions, args []string) error {
  690. if len(args) > 1 {
  691. return newOptionArgumentError("Invalid auto rotate arguments: %v", args)
  692. }
  693. po.AutoRotate = parseBoolOption(args[0])
  694. return nil
  695. }
  696. func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error {
  697. if len(args) > 1 {
  698. return newOptionArgumentError("Invalid enforce thumbnail arguments: %v", args)
  699. }
  700. po.EnforceThumbnail = parseBoolOption(args[0])
  701. return nil
  702. }
  703. func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error {
  704. if len(args) > 1 {
  705. return newOptionArgumentError("Invalid return_attachment arguments: %v", args)
  706. }
  707. po.ReturnAttachment = parseBoolOption(args[0])
  708. return nil
  709. }
  710. func applyMaxSrcResolutionOption(po *ProcessingOptions, args []string) error {
  711. if err := security.IsSecurityOptionsAllowed(); err != nil {
  712. return err
  713. }
  714. if len(args) > 1 {
  715. return newOptionArgumentError("Invalid max_src_resolution arguments: %v", args)
  716. }
  717. if x, err := strconv.ParseFloat(args[0], 64); err == nil && x > 0 {
  718. po.SecurityOptions.MaxSrcResolution = int(x * 1000000)
  719. } else {
  720. return newOptionArgumentError("Invalid max_src_resolution: %s", args[0])
  721. }
  722. return nil
  723. }
  724. func applyMaxSrcFileSizeOption(po *ProcessingOptions, args []string) error {
  725. if err := security.IsSecurityOptionsAllowed(); err != nil {
  726. return err
  727. }
  728. if len(args) > 1 {
  729. return newOptionArgumentError("Invalid max_src_file_size arguments: %v", args)
  730. }
  731. if x, err := strconv.Atoi(args[0]); err == nil {
  732. po.SecurityOptions.MaxSrcFileSize = x
  733. } else {
  734. return newOptionArgumentError("Invalid max_src_file_size: %s", args[0])
  735. }
  736. return nil
  737. }
  738. func applyMaxAnimationFramesOption(po *ProcessingOptions, args []string) error {
  739. if err := security.IsSecurityOptionsAllowed(); err != nil {
  740. return err
  741. }
  742. if len(args) > 1 {
  743. return newOptionArgumentError("Invalid max_animation_frames arguments: %v", args)
  744. }
  745. if x, err := strconv.Atoi(args[0]); err == nil && x > 0 {
  746. po.SecurityOptions.MaxAnimationFrames = x
  747. } else {
  748. return newOptionArgumentError("Invalid max_animation_frames: %s", args[0])
  749. }
  750. return nil
  751. }
  752. func applyMaxAnimationFrameResolutionOption(po *ProcessingOptions, args []string) error {
  753. if err := security.IsSecurityOptionsAllowed(); err != nil {
  754. return err
  755. }
  756. if len(args) > 1 {
  757. return newOptionArgumentError("Invalid max_animation_frame_resolution arguments: %v", args)
  758. }
  759. if x, err := strconv.ParseFloat(args[0], 64); err == nil {
  760. po.SecurityOptions.MaxAnimationFrameResolution = int(x * 1000000)
  761. } else {
  762. return newOptionArgumentError("Invalid max_animation_frame_resolution: %s", args[0])
  763. }
  764. return nil
  765. }
  766. func applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
  767. switch name {
  768. case "resize", "rs":
  769. return applyResizeOption(po, args)
  770. case "size", "s":
  771. return applySizeOption(po, args)
  772. case "resizing_type", "rt":
  773. return applyResizingTypeOption(po, args)
  774. case "width", "w":
  775. return applyWidthOption(po, args)
  776. case "height", "h":
  777. return applyHeightOption(po, args)
  778. case "min-width", "mw":
  779. return applyMinWidthOption(po, args)
  780. case "min-height", "mh":
  781. return applyMinHeightOption(po, args)
  782. case "zoom", "z":
  783. return applyZoomOption(po, args)
  784. case "dpr":
  785. return applyDprOption(po, args)
  786. case "enlarge", "el":
  787. return applyEnlargeOption(po, args)
  788. case "extend", "ex":
  789. return applyExtendOption(po, args)
  790. case "extend_aspect_ratio", "extend_ar", "exar":
  791. return applyExtendAspectRatioOption(po, args)
  792. case "gravity", "g":
  793. return applyGravityOption(po, args)
  794. case "crop", "c":
  795. return applyCropOption(po, args)
  796. case "trim", "t":
  797. return applyTrimOption(po, args)
  798. case "padding", "pd":
  799. return applyPaddingOption(po, args)
  800. case "auto_rotate", "ar":
  801. return applyAutoRotateOption(po, args)
  802. case "rotate", "rot":
  803. return applyRotateOption(po, args)
  804. case "background", "bg":
  805. return applyBackgroundOption(po, args)
  806. case "blur", "bl":
  807. return applyBlurOption(po, args)
  808. case "sharpen", "sh":
  809. return applySharpenOption(po, args)
  810. case "pixelate", "pix":
  811. return applyPixelateOption(po, args)
  812. case "watermark", "wm":
  813. return applyWatermarkOption(po, args)
  814. case "strip_metadata", "sm":
  815. return applyStripMetadataOption(po, args)
  816. case "keep_copyright", "kcr":
  817. return applyKeepCopyrightOption(po, args)
  818. case "strip_color_profile", "scp":
  819. return applyStripColorProfileOption(po, args)
  820. case "enforce_thumbnail", "eth":
  821. return applyEnforceThumbnailOption(po, args)
  822. // Saving options
  823. case "quality", "q":
  824. return applyQualityOption(po, args)
  825. case "format_quality", "fq":
  826. return applyFormatQualityOption(po, args)
  827. case "max_bytes", "mb":
  828. return applyMaxBytesOption(po, args)
  829. case "format", "f", "ext":
  830. return applyFormatOption(po, args)
  831. // Handling options
  832. case "skip_processing", "skp":
  833. return applySkipProcessingFormatsOption(po, args)
  834. case "raw":
  835. return applyRawOption(po, args)
  836. case "cachebuster", "cb":
  837. return applyCacheBusterOption(po, args)
  838. case "expires", "exp":
  839. return applyExpiresOption(po, args)
  840. case "filename", "fn":
  841. return applyFilenameOption(po, args)
  842. case "return_attachment", "att":
  843. return applyReturnAttachmentOption(po, args)
  844. // Presets
  845. case "preset", "pr":
  846. return applyPresetOption(po, args, usedPresets...)
  847. // Security
  848. case "max_src_resolution", "msr":
  849. return applyMaxSrcResolutionOption(po, args)
  850. case "max_src_file_size", "msfs":
  851. return applyMaxSrcFileSizeOption(po, args)
  852. case "max_animation_frames", "maf":
  853. return applyMaxAnimationFramesOption(po, args)
  854. case "max_animation_frame_resolution", "mafr":
  855. return applyMaxAnimationFrameResolutionOption(po, args)
  856. }
  857. return newUnknownOptionError("processing", name)
  858. }
  859. func applyURLOptions(po *ProcessingOptions, options urlOptions, usedPresets ...string) error {
  860. for _, opt := range options {
  861. if err := applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
  862. return err
  863. }
  864. }
  865. return nil
  866. }
  867. func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
  868. po := NewProcessingOptions()
  869. headerAccept := headers.Get("Accept")
  870. if strings.Contains(headerAccept, "image/webp") {
  871. po.PreferWebP = config.AutoWebp || config.EnforceWebp
  872. po.EnforceWebP = config.EnforceWebp
  873. }
  874. if strings.Contains(headerAccept, "image/avif") {
  875. po.PreferAvif = config.AutoAvif || config.EnforceAvif
  876. po.EnforceAvif = config.EnforceAvif
  877. }
  878. if strings.Contains(headerAccept, "image/jxl") {
  879. po.PreferJxl = config.AutoJxl || config.EnforceJxl
  880. po.EnforceJxl = config.EnforceJxl
  881. }
  882. if config.EnableClientHints {
  883. headerDPR := headers.Get("Sec-CH-DPR")
  884. if len(headerDPR) == 0 {
  885. headerDPR = headers.Get("DPR")
  886. }
  887. if len(headerDPR) > 0 {
  888. if dpr, err := strconv.ParseFloat(headerDPR, 64); err == nil && (dpr > 0 && dpr <= maxClientHintDPR) {
  889. po.Dpr = dpr
  890. }
  891. }
  892. headerWidth := headers.Get("Sec-CH-Width")
  893. if len(headerWidth) == 0 {
  894. headerWidth = headers.Get("Width")
  895. }
  896. if len(headerWidth) > 0 {
  897. if w, err := strconv.Atoi(headerWidth); err == nil {
  898. po.Width = imath.Shrink(w, po.Dpr)
  899. }
  900. }
  901. }
  902. if _, ok := presets["default"]; ok {
  903. if err := applyPresetOption(po, []string{"default"}); err != nil {
  904. return po, err
  905. }
  906. }
  907. return po, nil
  908. }
  909. func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
  910. if _, ok := resizeTypes[parts[0]]; ok {
  911. return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
  912. }
  913. po, err := defaultProcessingOptions(headers)
  914. if err != nil {
  915. return nil, "", err
  916. }
  917. options, urlParts := parseURLOptions(parts)
  918. if err = applyURLOptions(po, options); err != nil {
  919. return nil, "", err
  920. }
  921. url, extension, err := DecodeURL(urlParts)
  922. if err != nil {
  923. return nil, "", err
  924. }
  925. if !po.Raw && len(extension) > 0 {
  926. if err = applyFormatOption(po, []string{extension}); err != nil {
  927. return nil, "", err
  928. }
  929. }
  930. return po, url, nil
  931. }
  932. func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
  933. po, err := defaultProcessingOptions(headers)
  934. if err != nil {
  935. return nil, "", err
  936. }
  937. presets := strings.Split(parts[0], config.ArgumentsSeparator)
  938. urlParts := parts[1:]
  939. if err = applyPresetOption(po, presets); err != nil {
  940. return nil, "", err
  941. }
  942. url, extension, err := DecodeURL(urlParts)
  943. if err != nil {
  944. return nil, "", err
  945. }
  946. if !po.Raw && len(extension) > 0 {
  947. if err = applyFormatOption(po, []string{extension}); err != nil {
  948. return nil, "", err
  949. }
  950. }
  951. return po, url, nil
  952. }
  953. func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
  954. if path == "" || path == "/" {
  955. return nil, "", newInvalidURLError("Invalid path: %s", path)
  956. }
  957. parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
  958. var (
  959. imageURL string
  960. po *ProcessingOptions
  961. err error
  962. )
  963. if config.OnlyPresets {
  964. po, imageURL, err = parsePathPresets(parts, headers)
  965. } else {
  966. po, imageURL, err = parsePathOptions(parts, headers)
  967. }
  968. if err != nil {
  969. return nil, "", ierrors.Wrap(err, 0)
  970. }
  971. return po, imageURL, nil
  972. }