package gift import ( "image" "image/draw" "math" ) // Resampling is an interpolation algorithm used for image resizing. type Resampling interface { Support() float32 Kernel(float32) float32 } func bcspline(x, b, c float32) float32 { if x < 0 { x = -x } if x < 1 { return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6 } if x < 2 { return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6 } return 0 } func sinc(x float32) float32 { if x == 0 { return 1 } return float32(math.Sin(math.Pi*float64(x)) / (math.Pi * float64(x))) } type resamp struct { name string support float32 kernel func(float32) float32 } func (r resamp) String() string { return r.name } func (r resamp) Support() float32 { return r.support } func (r resamp) Kernel(x float32) float32 { return r.kernel(x) } // NearestNeighborResampling is a nearest neighbor resampling filter. var NearestNeighborResampling Resampling // BoxResampling is a box resampling filter (average of surrounding pixels). var BoxResampling Resampling // LinearResampling is a bilinear resampling filter. var LinearResampling Resampling // CubicResampling is a bicubic resampling filter (Catmull-Rom). var CubicResampling Resampling // LanczosResampling is a Lanczos resampling filter (3 lobes). var LanczosResampling Resampling type resampWeight struct { index int weight float32 } func prepareResampWeights(dstSize, srcSize int, resampling Resampling) [][]resampWeight { delta := float32(srcSize) / float32(dstSize) scale := delta if scale < 1 { scale = 1 } radius := float32(math.Ceil(float64(scale * resampling.Support()))) result := make([][]resampWeight, dstSize) tmp := make([]resampWeight, 0, dstSize*int(radius+2)*2) for i := 0; i < dstSize; i++ { center := (float32(i)+0.5)*delta - 0.5 left := int(math.Ceil(float64(center - radius))) if left < 0 { left = 0 } right := int(math.Floor(float64(center + radius))) if right > srcSize-1 { right = srcSize - 1 } var sum float32 for j := left; j <= right; j++ { weight := resampling.Kernel((float32(j) - center) / scale) if weight == 0 { continue } tmp = append(tmp, resampWeight{ index: j, weight: weight, }) sum += weight } for j := range tmp { tmp[j].weight /= sum } result[i] = tmp tmp = tmp[len(tmp):] } return result } func resizeLine(dst []pixel, src []pixel, weights [][]resampWeight) { for i := 0; i < len(dst); i++ { var r, g, b, a float32 for _, w := range weights[i] { c := src[w.index] wa := c.a * w.weight r += c.r * wa g += c.g * wa b += c.b * wa a += wa } if a != 0 { r /= a g /= a b /= a } dst[i] = pixel{r, g, b, a} } } func resizeHorizontal(dst draw.Image, src image.Image, w int, resampling Resampling, options *Options) { srcb := src.Bounds() dstb := dst.Bounds() weights := prepareResampWeights(w, srcb.Dx(), resampling) pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) { srcBuf := make([]pixel, srcb.Dx()) dstBuf := make([]pixel, w) for srcy := start; srcy < stop; srcy++ { pixGetter.getPixelRow(srcy, &srcBuf) resizeLine(dstBuf, srcBuf, weights) pixSetter.setPixelRow(dstb.Min.Y+srcy-srcb.Min.Y, dstBuf) } }) } func resizeVertical(dst draw.Image, src image.Image, h int, resampling Resampling, options *Options) { srcb := src.Bounds() dstb := dst.Bounds() weights := prepareResampWeights(h, srcb.Dy(), resampling) pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, srcb.Min.X, srcb.Max.X, func(start, stop int) { srcBuf := make([]pixel, srcb.Dy()) dstBuf := make([]pixel, h) for srcx := start; srcx < stop; srcx++ { pixGetter.getPixelColumn(srcx, &srcBuf) resizeLine(dstBuf, srcBuf, weights) pixSetter.setPixelColumn(dstb.Min.X+srcx-srcb.Min.X, dstBuf) } }) } func resizeNearest(dst draw.Image, src image.Image, w, h int, options *Options) { srcb := src.Bounds() dstb := dst.Bounds() dx := float64(srcb.Dx()) / float64(w) dy := float64(srcb.Dy()) / float64(h) pixGetter := newPixelGetter(src) pixSetter := newPixelSetter(dst) parallelize(options.Parallelization, dstb.Min.Y, dstb.Min.Y+h, func(start, stop int) { for dsty := start; dsty < stop; dsty++ { for dstx := dstb.Min.X; dstx < dstb.Min.X+w; dstx++ { fx := math.Floor((float64(dstx-dstb.Min.X) + 0.5) * dx) fy := math.Floor((float64(dsty-dstb.Min.Y) + 0.5) * dy) srcx := srcb.Min.X + int(fx) srcy := srcb.Min.Y + int(fy) px := pixGetter.getPixel(srcx, srcy) pixSetter.setPixel(dstx, dsty, px) } } }) } type resizeFilter struct { width int height int resampling Resampling } func (p *resizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) { w, h := p.width, p.height srcw, srch := srcBounds.Dx(), srcBounds.Dy() if (w == 0 && h == 0) || w < 0 || h < 0 || srcw <= 0 || srch <= 0 { dstBounds = image.Rect(0, 0, 0, 0) } else if w == 0 { fw := float64(h) * float64(srcw) / float64(srch) dstw := int(math.Max(1, math.Floor(fw+0.5))) dstBounds = image.Rect(0, 0, dstw, h) } else if h == 0 { fh := float64(w) * float64(srch) / float64(srcw) dsth := int(math.Max(1, math.Floor(fh+0.5))) dstBounds = image.Rect(0, 0, w, dsth) } else { dstBounds = image.Rect(0, 0, w, h) } return } func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *Options) { if options == nil { options = &defaultOptions } b := p.Bounds(src.Bounds()) w, h := b.Dx(), b.Dy() if w <= 0 || h <= 0 { return } if src.Bounds().Dx() == w && src.Bounds().Dy() == h { copyimage(dst, src, options) return } if p.resampling.Support() <= 0 { resizeNearest(dst, src, w, h, options) return } if src.Bounds().Dx() == w { resizeVertical(dst, src, h, p.resampling, options) return } if src.Bounds().Dy() == h { resizeHorizontal(dst, src, w, p.resampling, options) return } tmp := createTempImage(image.Rect(0, 0, w, src.Bounds().Dy())) resizeHorizontal(tmp, src, w, p.resampling, options) resizeVertical(dst, tmp, h, p.resampling, options) } // Resize creates a filter that resizes an image to the specified width and height using the specified resampling. // If one of width or height is 0, the image aspect ratio is preserved. // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. // // Example: // // // Resize the src image to width=300 preserving the aspect ratio. // g := gift.New( // gift.Resize(300, 0, gift.LanczosResampling), // ) // dst := image.NewRGBA(g.Bounds(src.Bounds())) // g.Draw(dst, src) // func Resize(width, height int, resampling Resampling) Filter { return &resizeFilter{ width: width, height: height, resampling: resampling, } } type resizeToFitFilter struct { width int height int resampling Resampling } func (p *resizeToFitFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { w, h := p.width, p.height srcw, srch := srcBounds.Dx(), srcBounds.Dy() if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 { return image.Rect(0, 0, 0, 0) } if srcw <= w && srch <= h { return image.Rect(0, 0, srcw, srch) } wratio := float64(srcw) / float64(w) hratio := float64(srch) / float64(h) var dstw, dsth int if wratio > hratio { dstw = w dsth = minint(int(float64(srch)/wratio+0.5), h) } else { dsth = h dstw = minint(int(float64(srcw)/hratio+0.5), w) } return image.Rect(0, 0, dstw, dsth) } func (p *resizeToFitFilter) Draw(dst draw.Image, src image.Image, options *Options) { b := p.Bounds(src.Bounds()) Resize(b.Dx(), b.Dy(), p.resampling).Draw(dst, src, options) } // ResizeToFit creates a filter that resizes an image to fit within the specified dimensions while preserving the aspect ratio. // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. func ResizeToFit(width, height int, resampling Resampling) Filter { return &resizeToFitFilter{ width: width, height: height, resampling: resampling, } } type resizeToFillFilter struct { width int height int anchor Anchor resampling Resampling } func (p *resizeToFillFilter) Bounds(srcBounds image.Rectangle) image.Rectangle { w, h := p.width, p.height srcw, srch := srcBounds.Dx(), srcBounds.Dy() if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 { return image.Rect(0, 0, 0, 0) } return image.Rect(0, 0, w, h) } func (p *resizeToFillFilter) Draw(dst draw.Image, src image.Image, options *Options) { b := p.Bounds(src.Bounds()) w, h := b.Dx(), b.Dy() if w <= 0 || h <= 0 { return } srcw, srch := src.Bounds().Dx(), src.Bounds().Dy() wratio := float64(srcw) / float64(w) hratio := float64(srch) / float64(h) var tmpw, tmph int if wratio < hratio { tmpw = w tmph = maxint(int(float64(srch)/wratio+0.5), h) } else { tmph = h tmpw = maxint(int(float64(srcw)/hratio+0.5), w) } tmp := createTempImage(image.Rect(0, 0, tmpw, tmph)) Resize(tmpw, tmph, p.resampling).Draw(tmp, src, options) CropToSize(w, h, p.anchor).Draw(dst, tmp, options) } // ResizeToFill creates a filter that resizes an image to the smallest possible size that will cover the specified dimensions, // then crops the resized image to the specified dimensions using the specified anchor point. // Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling. func ResizeToFill(width, height int, resampling Resampling, anchor Anchor) Filter { return &resizeToFillFilter{ width: width, height: height, anchor: anchor, resampling: resampling, } } func init() { // Nearest neighbor resampling filter. NearestNeighborResampling = resamp{ name: "NearestNeighborResampling", support: 0, kernel: func(x float32) float32 { return 0 }, } // Box resampling filter. BoxResampling = resamp{ name: "BoxResampling", support: 0.5, kernel: func(x float32) float32 { if x < 0 { x = -x } if x <= 0.5 { return 1 } return 0 }, } // Linear resampling filter. LinearResampling = resamp{ name: "LinearResampling", support: 1, kernel: func(x float32) float32 { if x < 0 { x = -x } if x < 1 { return 1 - x } return 0 }, } // Cubic resampling filter (Catmull-Rom). CubicResampling = resamp{ name: "CubicResampling", support: 2, kernel: func(x float32) float32 { if x < 0 { x = -x } if x < 2 { return bcspline(x, 0, 0.5) } return 0 }, } // Lanczos resampling filter (3 lobes). LanczosResampling = resamp{ name: "LanczosResampling", support: 3, kernel: func(x float32) float32 { if x < 0 { x = -x } if x < 3 { return sinc(x) * sinc(x/3) } return 0 }, } }