package transform import ( "image" "image/color" "math" "github.com/anthonynsimon/bild/clone" "github.com/anthonynsimon/bild/parallel" ) // RotationOptions are the rotation parameters // ResizeBounds set to false will keep the original image bounds, cutting any // pixels that go past it when rotating. // Pivot is the point of anchor for the rotation. Default of center is used if a nil is passed. // If ResizeBounds is set to true, a center pivot will always be used. type RotationOptions struct { ResizeBounds bool Pivot *image.Point } // Rotate returns a rotated image by the provided angle using the pivot as an anchor. // Parameters angle is in degrees and it's applied clockwise. // Default parameters are used if a nil *RotationOptions is passed. // // Usage example: // // // Rotate 90.0 degrees clockwise, preserving the image size and the pivot point at the top left corner // result := transform.Rotate(img, 90.0, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{0, 0}}) func Rotate(img image.Image, angle float64, options *RotationOptions) *image.RGBA { src := clone.AsShallowRGBA(img) srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy() supersample := false absAngle := int(math.Abs(angle) + 0.5) if absAngle%360 == 0 { // Return early if nothing to do return src } else if absAngle%90 != 0 { // Supersampling is required for non-special angles // Special angles = 90, 180, 270... supersample = true } // Config defaults resizeBounds := false // Default pivot position is center of image pivotX, pivotY := float64(srcW/2), float64(srcH/2) // Get options if provided if options != nil { resizeBounds = options.ResizeBounds if options.Pivot != nil { pivotX, pivotY = float64(options.Pivot.X), float64(options.Pivot.Y) } } if supersample { // Supersample, currently hard set to 2x srcW, srcH = srcW*2, srcH*2 src = Resize(src, srcW, srcH, NearestNeighbor) pivotX, pivotY = pivotX*2, pivotY*2 } // Convert to radians, positive degree maps to clockwise rotation angleRadians := -angle * (math.Pi / 180) var dstW, dstH int var sin, cos = math.Sincos(angleRadians) if resizeBounds { // Reserve larger size in destination image for full image bounds rotation // If not preserving size, always take image center as pivot pivotX, pivotY = float64(srcW)/2, float64(srcH)/2 a := math.Abs(float64(srcW) * sin) b := math.Abs(float64(srcW) * cos) c := math.Abs(float64(srcH) * sin) d := math.Abs(float64(srcH) * cos) dstW, dstH = int(c+b+0.5), int(a+d+0.5) } else { dstW, dstH = srcW, srcH } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) // Calculate offsets in case entire image is being displayed // Otherwise areas clipped by rotation won't be available offsetX := (dstW - srcW) / 2 offsetY := (dstH - srcH) / 2 parallel.Line(srcH, func(start, end int) { // Correct range to include the pixels visible in new bounds // Note that cannot be done in parallelize function input height, otherwise ranges would overlap yStart := int((float64(start)/float64(srcH))*float64(dstH)) - offsetY yEnd := int((float64(end)/float64(srcH))*float64(dstH)) - offsetY xStart := -offsetX xEnd := srcW + offsetX for y := yStart; y < yEnd; y++ { dy := float64(y) - pivotY + 0.5 for x := xStart; x < xEnd; x++ { dx := float64(x) - pivotX + 0.5 ix := int((cos*dx - sin*dy + pivotX)) iy := int((sin*dx + cos*dy + pivotY)) if ix < 0 || ix >= srcW || iy < 0 || iy >= srcH { continue } red, green, blue, alpha := src.At(ix, iy).RGBA() dst.Set(x+offsetX, y+offsetY, color.RGBA64{ R: uint16(red), G: uint16(green), B: uint16(blue), A: uint16(alpha), }) } } }) if supersample { // Downsample to original bounds as part of the Supersampling dst = Resize(dst, dstW/2, dstH/2, Linear) } return dst } // FlipH returns a horizontally flipped version of the image. func FlipH(img image.Image) *image.RGBA { bounds := img.Bounds() src := clone.AsShallowRGBA(img) dst := image.NewRGBA(bounds) w, h := dst.Bounds().Dx(), dst.Bounds().Dy() parallel.Line(h, func(start, end int) { for y := start; y < end; y++ { for x := 0; x < w; x++ { iy := y * dst.Stride pos := iy + (x * 4) flippedX := w - x - 1 flippedPos := iy + (flippedX * 4) dst.Pix[pos+0] = src.Pix[flippedPos+0] dst.Pix[pos+1] = src.Pix[flippedPos+1] dst.Pix[pos+2] = src.Pix[flippedPos+2] dst.Pix[pos+3] = src.Pix[flippedPos+3] } } }) return dst } // FlipV returns a vertically flipped version of the image. func FlipV(img image.Image) *image.RGBA { bounds := img.Bounds() src := clone.AsShallowRGBA(img) dst := image.NewRGBA(bounds) w, h := dst.Bounds().Dx(), dst.Bounds().Dy() parallel.Line(h, func(start, end int) { for y := start; y < end; y++ { for x := 0; x < w; x++ { pos := y*dst.Stride + (x * 4) flippedY := h - y - 1 flippedPos := flippedY*dst.Stride + (x * 4) dst.Pix[pos+0] = src.Pix[flippedPos+0] dst.Pix[pos+1] = src.Pix[flippedPos+1] dst.Pix[pos+2] = src.Pix[flippedPos+2] dst.Pix[pos+3] = src.Pix[flippedPos+3] } } }) return dst }