scale_on_load.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package processing
  2. import (
  3. "log/slog"
  4. "math"
  5. "github.com/imgproxy/imgproxy/v3/imagetype"
  6. "github.com/imgproxy/imgproxy/v3/imath"
  7. "github.com/imgproxy/imgproxy/v3/vips"
  8. )
  9. func (p *Processor) canScaleOnLoad(c *Context, shrink float64) bool {
  10. if c.ImgData == nil || shrink == 1 {
  11. return false
  12. }
  13. if c.ImgData.Format().IsVector() {
  14. return true
  15. }
  16. if p.config.DisableShrinkOnLoad || shrink <= 1 {
  17. return false
  18. }
  19. return c.ImgData.Format() == imagetype.JPEG ||
  20. c.ImgData.Format() == imagetype.WEBP ||
  21. c.ImgData.Format().SupportsThumbnail()
  22. }
  23. func calcJpegShink(shrink float64) float64 {
  24. switch {
  25. case shrink >= 8:
  26. return 8
  27. case shrink >= 4:
  28. return 4
  29. case shrink >= 2:
  30. return 2
  31. }
  32. return 1
  33. }
  34. func (p *Processor) scaleOnLoad(c *Context) error {
  35. // Get the preshrink value based on the requested scales.
  36. // We calculate it based on the image dimentions that we would get
  37. // with the current scales.
  38. // We can't just use c.WScale and c.HScale since this may lead to
  39. // overshrinking when only one target dimension is set.
  40. wshrink := float64(c.SrcWidth) / float64(imath.Scale(c.SrcWidth, c.WScale))
  41. hshrink := float64(c.SrcHeight) / float64(imath.Scale(c.SrcHeight, c.HScale))
  42. preshrink := min(wshrink, hshrink)
  43. // For vector images, apply the vector base shrink.
  44. // We might set it in the [Processor.vectorGuardScale] step in case the image
  45. // is too large.
  46. if c.ImgData != nil && c.ImgData.Format().IsVector() {
  47. preshrink *= c.VectorBaseShrink
  48. }
  49. // Check if we can and should scale the image on load
  50. if !p.canScaleOnLoad(c, preshrink) {
  51. return nil
  52. }
  53. // We will load the prescaled image into this new image.
  54. // On success, we will swap it with the original image in the context,
  55. // so we can safely clear it on function exit.
  56. newImg := new(vips.Image)
  57. defer newImg.Clear()
  58. loadThumbnail := c.ImgData.Format().SupportsThumbnail()
  59. if loadThumbnail {
  60. // If the image supports embedded thumbnails, try to load it
  61. if err := newImg.LoadThumbnail(c.ImgData); err != nil {
  62. slog.Debug("Can't load thumbnail: %s", "error", err)
  63. return nil
  64. }
  65. } else {
  66. // JPEG shrink-on-load must be 1, 2, 4 or 8.
  67. // We need to normalize it before passing to libvips.
  68. // For other formats, we can pass any float value.
  69. if c.ImgData.Format() == imagetype.JPEG {
  70. preshrink = calcJpegShink(preshrink)
  71. }
  72. // if preshrink is 1, we can skip reloading the image
  73. if preshrink == 1 {
  74. return nil
  75. }
  76. // Reload the image with preshrink
  77. if err := newImg.Load(c.ImgData, preshrink, 0, 1); err != nil {
  78. return err
  79. }
  80. }
  81. // Get the geometry of the preshrunk image
  82. newWidth, newHeight, newAngle, newFlip := ExtractGeometry(
  83. newImg, c.PO.Rotate(), c.PO.AutoRotate(),
  84. )
  85. // Calculate the actual preshrink values
  86. wpreshrink := float64(c.SrcWidth) / float64(newWidth)
  87. hpreshrink := float64(c.SrcHeight) / float64(newHeight)
  88. // If we loaded a thumbnail, check if it's worth using it
  89. if loadThumbnail {
  90. // If the thumbnail is not smaller than the original image or
  91. // if it is shrunk too much, we better keep the original image
  92. if min(wpreshrink, hpreshrink) <= 1.0 || max(wpreshrink, hpreshrink) > preshrink {
  93. return nil
  94. }
  95. }
  96. // Swap the image with the preshrunk one and update its orientation in the context
  97. c.Img.Swap(newImg)
  98. c.Angle = newAngle
  99. c.Flip = newFlip
  100. // Update scales after scale-on-load
  101. c.WScale *= wpreshrink
  102. c.HScale *= hpreshrink
  103. // If preshrink is exact, it's better to set scale to 1.0
  104. // to prevent additional scaling passes
  105. if newWidth == imath.Scale(newWidth, c.WScale) {
  106. c.WScale = 1.0
  107. }
  108. if newHeight == imath.Scale(newHeight, c.HScale) {
  109. c.HScale = 1.0
  110. }
  111. // We should crop before scaling, but we scaled the image on load,
  112. // so we need to adjust crop options
  113. if c.CropWidth > 0 {
  114. c.CropWidth = max(1, imath.Shrink(c.CropWidth, wpreshrink))
  115. }
  116. if c.CropHeight > 0 {
  117. c.CropHeight = max(1, imath.Shrink(c.CropHeight, hpreshrink))
  118. }
  119. // Adjust crop gravity offsets.
  120. // We don't need to adjust focus point offsets since they are always relative.
  121. // For other gravity types, we need to adjust only absolute offsets (>= 1.0 or <= -1.0).
  122. // We round absolute offsets to prevent turning them to relative (ex: 1.0 => 0.5).
  123. if c.CropGravity.Type != GravityFocusPoint {
  124. if math.Abs(c.CropGravity.X) >= 1.0 {
  125. c.CropGravity.X = math.RoundToEven(c.CropGravity.X / wpreshrink)
  126. }
  127. if math.Abs(c.CropGravity.Y) >= 1.0 {
  128. c.CropGravity.Y = math.RoundToEven(c.CropGravity.Y / hpreshrink)
  129. }
  130. }
  131. return nil
  132. }