rotate.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package transform
  2. import (
  3. "image"
  4. "image/color"
  5. "math"
  6. "github.com/anthonynsimon/bild/clone"
  7. "github.com/anthonynsimon/bild/parallel"
  8. )
  9. // RotationOptions are the rotation parameters
  10. // ResizeBounds set to false will keep the original image bounds, cutting any
  11. // pixels that go past it when rotating.
  12. // Pivot is the point of anchor for the rotation. Default of center is used if a nil is passed.
  13. // If ResizeBounds is set to true, a center pivot will always be used.
  14. type RotationOptions struct {
  15. ResizeBounds bool
  16. Pivot *image.Point
  17. }
  18. // Rotate returns a rotated image by the provided angle using the pivot as an anchor.
  19. // Parameters angle is in degrees and it's applied clockwise.
  20. // Default parameters are used if a nil *RotationOptions is passed.
  21. //
  22. // Usage example:
  23. //
  24. // // Rotate 90.0 degrees clockwise, preserving the image size and the pivot point at the top left corner
  25. // result := transform.Rotate(img, 90.0, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{0, 0}})
  26. func Rotate(img image.Image, angle float64, options *RotationOptions) *image.RGBA {
  27. src := clone.AsShallowRGBA(img)
  28. srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy()
  29. supersample := false
  30. absAngle := int(math.Abs(angle) + 0.5)
  31. if absAngle%360 == 0 {
  32. // Return early if nothing to do
  33. return src
  34. } else if absAngle%90 != 0 {
  35. // Supersampling is required for non-special angles
  36. // Special angles = 90, 180, 270...
  37. supersample = true
  38. }
  39. // Config defaults
  40. resizeBounds := false
  41. // Default pivot position is center of image
  42. pivotX, pivotY := float64(srcW/2), float64(srcH/2)
  43. // Get options if provided
  44. if options != nil {
  45. resizeBounds = options.ResizeBounds
  46. if options.Pivot != nil {
  47. pivotX, pivotY = float64(options.Pivot.X), float64(options.Pivot.Y)
  48. }
  49. }
  50. if supersample {
  51. // Supersample, currently hard set to 2x
  52. srcW, srcH = srcW*2, srcH*2
  53. src = Resize(src, srcW, srcH, NearestNeighbor)
  54. pivotX, pivotY = pivotX*2, pivotY*2
  55. }
  56. // Convert to radians, positive degree maps to clockwise rotation
  57. angleRadians := -angle * (math.Pi / 180)
  58. var dstW, dstH int
  59. var sin, cos = math.Sincos(angleRadians)
  60. if resizeBounds {
  61. // Reserve larger size in destination image for full image bounds rotation
  62. // If not preserving size, always take image center as pivot
  63. pivotX, pivotY = float64(srcW)/2, float64(srcH)/2
  64. a := math.Abs(float64(srcW) * sin)
  65. b := math.Abs(float64(srcW) * cos)
  66. c := math.Abs(float64(srcH) * sin)
  67. d := math.Abs(float64(srcH) * cos)
  68. dstW, dstH = int(c+b+0.5), int(a+d+0.5)
  69. } else {
  70. dstW, dstH = srcW, srcH
  71. }
  72. dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
  73. // Calculate offsets in case entire image is being displayed
  74. // Otherwise areas clipped by rotation won't be available
  75. offsetX := (dstW - srcW) / 2
  76. offsetY := (dstH - srcH) / 2
  77. parallel.Line(srcH, func(start, end int) {
  78. // Correct range to include the pixels visible in new bounds
  79. // Note that cannot be done in parallelize function input height, otherwise ranges would overlap
  80. yStart := int((float64(start)/float64(srcH))*float64(dstH)) - offsetY
  81. yEnd := int((float64(end)/float64(srcH))*float64(dstH)) - offsetY
  82. xStart := -offsetX
  83. xEnd := srcW + offsetX
  84. for y := yStart; y < yEnd; y++ {
  85. dy := float64(y) - pivotY + 0.5
  86. for x := xStart; x < xEnd; x++ {
  87. dx := float64(x) - pivotX + 0.5
  88. ix := int((cos*dx - sin*dy + pivotX))
  89. iy := int((sin*dx + cos*dy + pivotY))
  90. if ix < 0 || ix >= srcW || iy < 0 || iy >= srcH {
  91. continue
  92. }
  93. red, green, blue, alpha := src.At(ix, iy).RGBA()
  94. dst.Set(x+offsetX, y+offsetY, color.RGBA64{
  95. R: uint16(red),
  96. G: uint16(green),
  97. B: uint16(blue),
  98. A: uint16(alpha),
  99. })
  100. }
  101. }
  102. })
  103. if supersample {
  104. // Downsample to original bounds as part of the Supersampling
  105. dst = Resize(dst, dstW/2, dstH/2, Linear)
  106. }
  107. return dst
  108. }
  109. // FlipH returns a horizontally flipped version of the image.
  110. func FlipH(img image.Image) *image.RGBA {
  111. bounds := img.Bounds()
  112. src := clone.AsShallowRGBA(img)
  113. dst := image.NewRGBA(bounds)
  114. w, h := dst.Bounds().Dx(), dst.Bounds().Dy()
  115. parallel.Line(h, func(start, end int) {
  116. for y := start; y < end; y++ {
  117. for x := 0; x < w; x++ {
  118. iy := y * dst.Stride
  119. pos := iy + (x * 4)
  120. flippedX := w - x - 1
  121. flippedPos := iy + (flippedX * 4)
  122. dst.Pix[pos+0] = src.Pix[flippedPos+0]
  123. dst.Pix[pos+1] = src.Pix[flippedPos+1]
  124. dst.Pix[pos+2] = src.Pix[flippedPos+2]
  125. dst.Pix[pos+3] = src.Pix[flippedPos+3]
  126. }
  127. }
  128. })
  129. return dst
  130. }
  131. // FlipV returns a vertically flipped version of the image.
  132. func FlipV(img image.Image) *image.RGBA {
  133. bounds := img.Bounds()
  134. src := clone.AsShallowRGBA(img)
  135. dst := image.NewRGBA(bounds)
  136. w, h := dst.Bounds().Dx(), dst.Bounds().Dy()
  137. parallel.Line(h, func(start, end int) {
  138. for y := start; y < end; y++ {
  139. for x := 0; x < w; x++ {
  140. pos := y*dst.Stride + (x * 4)
  141. flippedY := h - y - 1
  142. flippedPos := flippedY*dst.Stride + (x * 4)
  143. dst.Pix[pos+0] = src.Pix[flippedPos+0]
  144. dst.Pix[pos+1] = src.Pix[flippedPos+1]
  145. dst.Pix[pos+2] = src.Pix[flippedPos+2]
  146. dst.Pix[pos+3] = src.Pix[flippedPos+3]
  147. }
  148. }
  149. })
  150. return dst
  151. }