processing_options.go 31 KB

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