462 lines
11 KiB
Go
462 lines
11 KiB
Go
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
|
|
},
|
|
}
|
|
}
|