processing.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package processing
  2. import (
  3. "context"
  4. "fmt"
  5. "runtime"
  6. "strconv"
  7. log "github.com/sirupsen/logrus"
  8. "github.com/imgproxy/imgproxy/v3/config"
  9. "github.com/imgproxy/imgproxy/v3/imagedata"
  10. "github.com/imgproxy/imgproxy/v3/imagetype"
  11. "github.com/imgproxy/imgproxy/v3/imath"
  12. "github.com/imgproxy/imgproxy/v3/options"
  13. "github.com/imgproxy/imgproxy/v3/router"
  14. "github.com/imgproxy/imgproxy/v3/security"
  15. "github.com/imgproxy/imgproxy/v3/vips"
  16. )
  17. var mainPipeline = pipeline{
  18. trim,
  19. prepare,
  20. scaleOnLoad,
  21. importColorProfile,
  22. crop,
  23. scale,
  24. rotateAndFlip,
  25. cropToResult,
  26. fixWebpSize,
  27. applyFilters,
  28. extend,
  29. padding,
  30. flatten,
  31. watermark,
  32. exportColorProfile,
  33. finalize,
  34. }
  35. func imageTypeGoodForWeb(imgtype imagetype.Type) bool {
  36. return imgtype != imagetype.TIFF &&
  37. imgtype != imagetype.BMP
  38. }
  39. func canFitToBytes(imgtype imagetype.Type) bool {
  40. switch imgtype {
  41. case imagetype.JPEG, imagetype.WEBP, imagetype.AVIF, imagetype.TIFF:
  42. return true
  43. default:
  44. return false
  45. }
  46. }
  47. func getImageSize(img *vips.Image) (int, int) {
  48. width, height, _, _ := extractMeta(img, 0, true)
  49. if pages, err := img.GetIntDefault("n-pages", 1); err != nil && pages > 0 {
  50. height /= pages
  51. }
  52. return width, height
  53. }
  54. func transformAnimated(ctx context.Context, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
  55. if po.Trim.Enabled {
  56. log.Warning("Trim is not supported for animated images")
  57. po.Trim.Enabled = false
  58. }
  59. imgWidth := img.Width()
  60. frameHeight, err := img.GetInt("page-height")
  61. if err != nil {
  62. return err
  63. }
  64. framesCount := imath.Min(img.Height()/frameHeight, config.MaxAnimationFrames)
  65. // Double check dimensions because animated image has many frames
  66. if err = security.CheckDimensions(imgWidth, frameHeight*framesCount); err != nil {
  67. return err
  68. }
  69. // Vips 8.8+ supports n-pages and doesn't load the whole animated image on header access
  70. if nPages, _ := img.GetIntDefault("n-pages", 1); nPages > framesCount {
  71. // Load only the needed frames
  72. if err = img.Load(imgdata, 1, 1.0, framesCount); err != nil {
  73. return err
  74. }
  75. }
  76. delay, err := img.GetIntSliceDefault("delay", nil)
  77. if err != nil {
  78. return err
  79. }
  80. loop, err := img.GetIntDefault("loop", 0)
  81. if err != nil {
  82. return err
  83. }
  84. watermarkEnabled := po.Watermark.Enabled
  85. po.Watermark.Enabled = false
  86. defer func() { po.Watermark.Enabled = watermarkEnabled }()
  87. frames := make([]*vips.Image, 0, framesCount)
  88. defer func() {
  89. for _, frame := range frames {
  90. if frame != nil {
  91. frame.Clear()
  92. }
  93. }
  94. }()
  95. for i := 0; i < framesCount; i++ {
  96. frame := new(vips.Image)
  97. if err = img.Extract(frame, 0, i*frameHeight, imgWidth, frameHeight); err != nil {
  98. return err
  99. }
  100. frames = append(frames, frame)
  101. if err = mainPipeline.Run(ctx, frame, po, nil); err != nil {
  102. return err
  103. }
  104. }
  105. if err = img.Arrayjoin(frames); err != nil {
  106. return err
  107. }
  108. if watermarkEnabled && imagedata.Watermark != nil {
  109. if err = applyWatermark(img, imagedata.Watermark, &po.Watermark, framesCount); err != nil {
  110. return err
  111. }
  112. }
  113. if err = img.CastUchar(); err != nil {
  114. return err
  115. }
  116. if err = copyMemoryAndCheckTimeout(ctx, img); err != nil {
  117. return err
  118. }
  119. if len(delay) == 0 {
  120. delay = make([]int, framesCount)
  121. for i := range delay {
  122. delay[i] = 40
  123. }
  124. } else if len(delay) > framesCount {
  125. delay = delay[:framesCount]
  126. }
  127. img.SetInt("page-height", frames[0].Height())
  128. img.SetIntSlice("delay", delay)
  129. img.SetInt("loop", loop)
  130. img.SetInt("n-pages", framesCount)
  131. return nil
  132. }
  133. func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img *vips.Image) (*imagedata.ImageData, error) {
  134. var diff float64
  135. quality := po.GetQuality()
  136. for {
  137. imgdata, err := img.Save(po.Format, quality)
  138. if len(imgdata.Data) <= po.MaxBytes || quality <= 10 || err != nil {
  139. return imgdata, err
  140. }
  141. imgdata.Close()
  142. router.CheckTimeout(ctx)
  143. delta := float64(len(imgdata.Data)) / float64(po.MaxBytes)
  144. switch {
  145. case delta > 3:
  146. diff = 0.25
  147. case delta > 1.5:
  148. diff = 0.5
  149. default:
  150. diff = 0.75
  151. }
  152. quality = int(float64(quality) * diff)
  153. }
  154. }
  155. func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options.ProcessingOptions) (*imagedata.ImageData, error) {
  156. runtime.LockOSThread()
  157. defer runtime.UnlockOSThread()
  158. defer vips.Cleanup()
  159. animationSupport :=
  160. config.MaxAnimationFrames > 1 &&
  161. imgdata.Type.SupportsAnimation() &&
  162. (po.Format == imagetype.Unknown || po.Format.SupportsAnimation())
  163. pages := 1
  164. if animationSupport {
  165. pages = -1
  166. }
  167. img := new(vips.Image)
  168. defer img.Clear()
  169. if po.EnforceThumbnail && imgdata.Type.SupportsThumbnail() {
  170. if err := img.LoadThumbnail(imgdata); err != nil {
  171. return nil, err
  172. }
  173. } else {
  174. if err := img.Load(imgdata, 1, 1.0, pages); err != nil {
  175. return nil, err
  176. }
  177. }
  178. originWidth, originHeight := getImageSize(img)
  179. animated := img.IsAnimated()
  180. switch {
  181. case po.Format == imagetype.Unknown:
  182. switch {
  183. case po.PreferAvif && !animated:
  184. po.Format = imagetype.AVIF
  185. case po.PreferWebP:
  186. po.Format = imagetype.WEBP
  187. case vips.SupportsSave(imgdata.Type) && imageTypeGoodForWeb(imgdata.Type):
  188. po.Format = imgdata.Type
  189. default:
  190. po.Format = imagetype.JPEG
  191. }
  192. case po.EnforceAvif && !animated:
  193. po.Format = imagetype.AVIF
  194. case po.EnforceWebP:
  195. po.Format = imagetype.WEBP
  196. }
  197. if !vips.SupportsSave(po.Format) {
  198. return nil, fmt.Errorf("Can't save %s, probably not supported by your libvips", po.Format)
  199. }
  200. if po.Format.SupportsAnimation() && animated {
  201. if err := transformAnimated(ctx, img, po, imgdata); err != nil {
  202. return nil, err
  203. }
  204. } else {
  205. if animated {
  206. // We loaded animated image but the resulting format doesn't support
  207. // animations, so we need to reload image as not animated
  208. if err := img.Load(imgdata, 1, 1.0, 1); err != nil {
  209. return nil, err
  210. }
  211. }
  212. if err := mainPipeline.Run(ctx, img, po, imgdata); err != nil {
  213. return nil, err
  214. }
  215. }
  216. if err := copyMemoryAndCheckTimeout(ctx, img); err != nil {
  217. return nil, err
  218. }
  219. if po.Format == imagetype.AVIF && (img.Width() < 16 || img.Height() < 16) {
  220. if img.HasAlpha() {
  221. po.Format = imagetype.PNG
  222. } else {
  223. po.Format = imagetype.JPEG
  224. }
  225. log.Warningf(
  226. "Minimal dimension of AVIF is 16, current image size is %dx%d. Image will be saved as %s",
  227. img.Width(), img.Height(), po.Format,
  228. )
  229. }
  230. var (
  231. outData *imagedata.ImageData
  232. err error
  233. )
  234. if po.MaxBytes > 0 && canFitToBytes(po.Format) {
  235. outData, err = saveImageToFitBytes(ctx, po, img)
  236. } else {
  237. outData, err = img.Save(po.Format, po.GetQuality())
  238. }
  239. if err == nil {
  240. if outData.Headers == nil {
  241. outData.Headers = make(map[string]string)
  242. }
  243. outData.Headers["X-Origin-Width"] = strconv.Itoa(originWidth)
  244. outData.Headers["X-Origin-Height"] = strconv.Itoa(originHeight)
  245. outData.Headers["X-Result-Width"] = strconv.Itoa(img.Width())
  246. outData.Headers["X-Result-Height"] = strconv.Itoa(img.Height())
  247. }
  248. return outData, err
  249. }