package gift import ( "image" "image/color" "image/draw" ) type transformType int const ( ttRotate90 transformType = iota ttRotate180 ttRotate270 ttFlipHorizontal ttFlipVertical ttTranspose ttTransverse ) type transformFilter struct { tt transformType } func (p *transformFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { if p.tt == ttRotate90 || p.tt == ttRotate270 || p.tt == ttTranspose || p.tt == ttTransverse { dstBounds = image.Rect(0, 0, srcBounds.Dy(), srcBounds.Dx()) } else { dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy()) } return } func (p *transformFilter) Draw(dst draw.Image, src image.Image, options *Options) { if options == nil { options = &defaultOptions } srcb := src.Bounds() dstb := dst.Bounds() pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { for srcy := start; srcy < stop; srcy++ { for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ { var dstx, dsty int switch p.tt { case ttRotate90: dstx = dstb.Min.X + srcy - srcb.Min.Y dsty = dstb.Min.Y + srcb.Max.X - srcx - 1 case ttRotate180: dstx = dstb.Min.X + srcb.Max.X - srcx - 1 dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1 case ttRotate270: dstx = dstb.Min.X + srcb.Max.Y - srcy - 1 dsty = dstb.Min.Y + srcx - srcb.Min.X case ttFlipHorizontal: dstx = dstb.Min.X + srcb.Max.X - srcx - 1 dsty = dstb.Min.Y + srcy - srcb.Min.Y case ttFlipVertical: dstx = dstb.Min.X + srcx - srcb.Min.X dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1 case ttTranspose: dstx = dstb.Min.X + srcy - srcb.Min.Y dsty = dstb.Min.Y + srcx - srcb.Min.X case ttTransverse: dstx = dstb.Min.Y + srcb.Max.Y - srcy - 1 dsty = dstb.Min.X + srcb.Max.X - srcx - 1 } pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy)) } } }) } // Rotate90 creates a filter that rotates an image 90 degrees counter-clockwise. func Rotate90() Filter { return &transformFilter{ tt: ttRotate90, } } // Rotate180 creates a filter that rotates an image 180 degrees counter-clockwise. func Rotate180() Filter { return &transformFilter{ tt: ttRotate180, } } // Rotate270 creates a filter that rotates an image 270 degrees counter-clockwise. func Rotate270() Filter { return &transformFilter{ tt: ttRotate270, } } // FlipHorizontal creates a filter that flips an image horizontally. func FlipHorizontal() Filter { return &transformFilter{ tt: ttFlipHorizontal, } } // FlipVertical creates a filter that flips an image vertically. func FlipVertical() Filter { return &transformFilter{ tt: ttFlipVertical, } } // Transpose creates a filter that flips an image horizontally and rotates 90 degrees counter-clockwise. func Transpose() Filter { return &transformFilter{ tt: ttTranspose, } } // Transverse creates a filter that flips an image vertically and rotates 90 degrees counter-clockwise. func Transverse() Filter { return &transformFilter{ tt: ttTransverse, } } // Interpolation is an interpolation algorithm used for image transformation. type Interpolation int const ( // NearestNeighborInterpolation is a nearest-neighbor interpolation algorithm. NearestNeighborInterpolation Interpolation = iota // LinearInterpolation is a bilinear interpolation algorithm. LinearInterpolation // CubicInterpolation is a bicubic interpolation algorithm. CubicInterpolation ) func rotatePoint(x, y, asin, acos float32) (float32, float32) { newx := x*acos - y*asin newy := x*asin + y*acos return newx, newy } func calcRotatedSize(w, h int, angle float32) (int, int) { if w <= 0 || h <= 0 { return 0, 0 } xoff := float32(w)/2 - 0.5 yoff := float32(h)/2 - 0.5 asin, acos := sincosf32(angle) x1, y1 := rotatePoint(0-xoff, 0-yoff, asin, acos) x2, y2 := rotatePoint(float32(w-1)-xoff, 0-yoff, asin, acos) x3, y3 := rotatePoint(float32(w-1)-xoff, float32(h-1)-yoff, asin, acos) x4, y4 := rotatePoint(0-xoff, float32(h-1)-yoff, asin, acos) minx := minf32(x1, minf32(x2, minf32(x3, x4))) maxx := maxf32(x1, maxf32(x2, maxf32(x3, x4))) miny := minf32(y1, minf32(y2, minf32(y3, y4))) maxy := maxf32(y1, maxf32(y2, maxf32(y3, y4))) neww := maxx - minx + 1 if neww-floorf32(neww) > 0.01 { neww += 2 } newh := maxy - miny + 1 if newh-floorf32(newh) > 0.01 { newh += 2 } return int(neww), int(newh) } type rotateFilter struct { angle float32 bgcolor color.Color interpolation Interpolation } func (p *rotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { w, h := calcRotatedSize(srcBounds.Dx(), srcBounds.Dy(), p.angle) dstBounds = image.Rect(0, 0, w, h) return } func (p *rotateFilter) Draw(dst draw.Image, src image.Image, options *Options) { if options == nil { options = &defaultOptions } srcb := src.Bounds() dstb := dst.Bounds() w, h := calcRotatedSize(srcb.Dx(), srcb.Dy(), p.angle) if w <= 0 || h <= 0 { return } srcxoff := float32(srcb.Dx())/2 - 0.5 srcyoff := float32(srcb.Dy())/2 - 0.5 dstxoff := float32(w)/2 - 0.5 dstyoff := float32(h)/2 - 0.5 bgpx := pixelFromColor(p.bgcolor) asin, acos := sincosf32(p.angle) pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, 0, h, func(start, stop int) { for y := start; y < stop; y++ { for x := 0; x < w; x++ { xf, yf := rotatePoint(float32(x)-dstxoff, float32(y)-dstyoff, asin, acos) xf, yf = float32(srcb.Min.X)+xf+srcxoff, float32(srcb.Min.Y)+yf+srcyoff var px pixel switch p.interpolation { case CubicInterpolation: px = interpolateCubic(xf, yf, srcb, pixGetter, bgpx) case LinearInterpolation: px = interpolateLinear(xf, yf, srcb, pixGetter, bgpx) default: px = interpolateNearest(xf, yf, srcb, pixGetter, bgpx) } pixSetter.setPixel(dstb.Min.X+x, dstb.Min.Y+y, px) } } }) } func interpolateCubic(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { var pxs [16]pixel var cfs [16]float32 var px pixel x0, y0 := int(floorf32(xf)), int(floorf32(yf)) if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) { return bgpx } xq, yq := xf-float32(x0), yf-float32(y0) for i := 0; i < 4; i++ { for j := 0; j < 4; j++ { pt := image.Pt(x0+j-1, y0+i-1) if pt.In(bounds) { pxs[i*4+j] = pixGetter.getPixel(pt.X, pt.Y) } else { pxs[i*4+j] = bgpx } } } const ( k04 = 1 / 4.0 k12 = 1 / 12.0 k36 = 1 / 36.0 ) cfs[0] = k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) cfs[1] = -k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) cfs[2] = k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) cfs[3] = -k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) cfs[4] = -k12 * xq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1) cfs[5] = k04 * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1) cfs[6] = -k04 * xq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1) cfs[7] = k12 * xq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1) cfs[8] = k12 * xq * yq * (xq - 1) * (xq - 2) * (yq + 1) * (yq - 2) cfs[9] = -k04 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq + 1) * (yq - 2) cfs[10] = k04 * xq * yq * (xq + 1) * (xq - 2) * (yq + 1) * (yq - 2) cfs[11] = -k12 * xq * yq * (xq - 1) * (xq + 1) * (yq + 1) * (yq - 2) cfs[12] = -k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq + 1) cfs[13] = k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq + 1) cfs[14] = -k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq + 1) cfs[15] = k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq + 1) for i := range pxs { wa := pxs[i].a * cfs[i] px.r += pxs[i].r * wa px.g += pxs[i].g * wa px.b += pxs[i].b * wa px.a += wa } if px.a != 0 { px.r /= px.a px.g /= px.a px.b /= px.a } return px } func interpolateLinear(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { var pxs [4]pixel var cfs [4]float32 var px pixel x0, y0 := int(floorf32(xf)), int(floorf32(yf)) if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) { return bgpx } xq, yq := xf-float32(x0), yf-float32(y0) for i := 0; i < 2; i++ { for j := 0; j < 2; j++ { pt := image.Pt(x0+j, y0+i) if pt.In(bounds) { pxs[i*2+j] = pixGetter.getPixel(pt.X, pt.Y) } else { pxs[i*2+j] = bgpx } } } cfs[0] = (1 - xq) * (1 - yq) cfs[1] = xq * (1 - yq) cfs[2] = (1 - xq) * yq cfs[3] = xq * yq for i := range pxs { wa := pxs[i].a * cfs[i] px.r += pxs[i].r * wa px.g += pxs[i].g * wa px.b += pxs[i].b * wa px.a += wa } if px.a != 0 { px.r /= px.a px.g /= px.a px.b /= px.a } return px } func interpolateNearest(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel { x0, y0 := int(floorf32(xf+0.5)), int(floorf32(yf+0.5)) if image.Pt(x0, y0).In(bounds) { return pixGetter.getPixel(x0, y0) } return bgpx } // Rotate creates a filter that rotates an image by the given angle counter-clockwise. // The angle parameter is the rotation angle in degrees. // The backgroundColor parameter specifies the color of the uncovered zone after the rotation. // The interpolation parameter specifies the interpolation method. // Supported interpolation methods: NearestNeighborInterpolation, LinearInterpolation, CubicInterpolation. // // Example: // // g := gift.New( // gift.Rotate(45, color.Black, gift.LinearInterpolation), // ) // dst := image.NewRGBA(g.Bounds(src.Bounds())) // g.Draw(dst, src) // func Rotate(angle float32, backgroundColor color.Color, interpolation Interpolation) Filter { return &rotateFilter{ angle: angle, bgcolor: backgroundColor, interpolation: interpolation, } } type cropFilter struct { rect image.Rectangle } func (p *cropFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { b := srcBounds.Intersect(p.rect) return b.Sub(b.Min) } func (p *cropFilter) Draw(dst draw.Image, src image.Image, options *Options) { if options == nil { options = &defaultOptions } srcb := src.Bounds().Intersect(p.rect) dstb := dst.Bounds() pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { for srcy := start; srcy < stop; srcy++ { for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ { dstx := dstb.Min.X + srcx - srcb.Min.X dsty := dstb.Min.Y + srcy - srcb.Min.Y pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy)) } } }) } // Crop creates a filter that crops the specified rectangular region from an image. // // Example: // // g := gift.New( // gift.Crop(image.Rect(100, 100, 200, 200)), // ) // dst := image.NewRGBA(g.Bounds(src.Bounds())) // g.Draw(dst, src) // func Crop(rect image.Rectangle) Filter { return &cropFilter{ rect: rect, } } // Anchor is the anchor point for image cropping. type Anchor int // Anchor point positions. const ( CenterAnchor Anchor = iota TopLeftAnchor TopAnchor TopRightAnchor LeftAnchor RightAnchor BottomLeftAnchor BottomAnchor BottomRightAnchor ) func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point { var x, y int switch anchor { case TopLeftAnchor: x = b.Min.X y = b.Min.Y case TopAnchor: x = b.Min.X + (b.Dx()-w)/2 y = b.Min.Y case TopRightAnchor: x = b.Max.X - w y = b.Min.Y case LeftAnchor: x = b.Min.X y = b.Min.Y + (b.Dy()-h)/2 case RightAnchor: x = b.Max.X - w y = b.Min.Y + (b.Dy()-h)/2 case BottomLeftAnchor: x = b.Min.X y = b.Max.Y - h case BottomAnchor: x = b.Min.X + (b.Dx()-w)/2 y = b.Max.Y - h case BottomRightAnchor: x = b.Max.X - w y = b.Max.Y - h default: x = b.Min.X + (b.Dx()-w)/2 y = b.Min.Y + (b.Dy()-h)/2 } return image.Pt(x, y) } type cropToSizeFilter struct { w, h int anchor Anchor } func (p *cropToSizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { if p.w <= 0 || p.h <= 0 { return image.Rect(0, 0, 0, 0) } pt := anchorPt(srcBounds, p.w, p.h, p.anchor) r := image.Rect(0, 0, p.w, p.h).Add(pt) b := srcBounds.Intersect(r) return b.Sub(b.Min) } func (p *cropToSizeFilter) Draw(dst draw.Image, src image.Image, options *Options) { if p.w <= 0 || p.h <= 0 { return } pt := anchorPt(src.Bounds(), p.w, p.h, p.anchor) r := image.Rect(0, 0, p.w, p.h).Add(pt) b := src.Bounds().Intersect(r) Crop(b).Draw(dst, src, options) } // CropToSize creates a filter that crops an image to the specified size using the specified anchor point. func CropToSize(width, height int, anchor Anchor) Filter { return &cropToSizeFilter{ w: width, h: height, anchor: anchor, } }