save_fit_bytes.go 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. package processing
  2. import (
  3. "context"
  4. "github.com/imgproxy/imgproxy/v3/imagedata"
  5. "github.com/imgproxy/imgproxy/v3/imagetype"
  6. "github.com/imgproxy/imgproxy/v3/imath"
  7. "github.com/imgproxy/imgproxy/v3/server"
  8. "github.com/imgproxy/imgproxy/v3/vips"
  9. )
  10. // saveImageToFitBytes tries to save the image to fit into the specified max bytes
  11. // by lowering the quality. It returns the image data that fits the requirement
  12. // or the best effort data if it was not possible to fit into the limit.
  13. func saveImageToFitBytes(
  14. ctx context.Context,
  15. img *vips.Image,
  16. format imagetype.Type,
  17. startQuality int,
  18. target int,
  19. ) (imagedata.ImageData, error) {
  20. var newQuality int
  21. // Start with the specified quality and go down from there.
  22. quality := startQuality
  23. // We will probably save the image multiple times, so we need to process its pixels
  24. // to ensure that it is in random access mode.
  25. if err := img.CopyMemory(); err != nil {
  26. return nil, err
  27. }
  28. for {
  29. // Check for timeout or cancellation before each attempt as we might spend too much
  30. // time processing the image or making previous attempts.
  31. if err := server.CheckTimeout(ctx); err != nil {
  32. return nil, err
  33. }
  34. imgdata, err := img.Save(format, quality)
  35. if err != nil {
  36. return nil, err
  37. }
  38. size, err := imgdata.Size()
  39. if err != nil {
  40. imgdata.Close()
  41. return nil, err
  42. }
  43. // If we fit the limit or quality is too low, return the result.
  44. if size <= target || quality <= 10 {
  45. return imgdata, err
  46. }
  47. // We don't need the image data anymore, close it to free resources.
  48. imgdata.Close()
  49. // Tune quality for the next attempt based on how much we exceed the limit.
  50. delta := float64(size) / float64(target)
  51. switch {
  52. case delta > 3:
  53. newQuality = imath.Scale(quality, 0.25)
  54. case delta > 1.5:
  55. newQuality = imath.Scale(quality, 0.5)
  56. default:
  57. newQuality = imath.Scale(quality, 0.75)
  58. }
  59. // Ensure that quality is always lowered, even if the scaling
  60. // doesn't change it due to rounding.
  61. // Also, ensure that quality doesn't go below the minimum.
  62. quality = max(1, min(quality-1, newQuality))
  63. }
  64. }