save_fit_bytes.go 2.1 KB

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