1package gift
2
3import (
4 "image"
5 "image/draw"
6 "math"
7)
8
9// Resampling is an interpolation algorithm used for image resizing.
10type Resampling interface {
11 Support() float32
12 Kernel(float32) float32
13}
14
15func bcspline(x, b, c float32) float32 {
16 if x < 0 {
17 x = -x
18 }
19 if x < 1 {
20 return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
21 }
22 if x < 2 {
23 return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
24 }
25 return 0
26}
27
28func sinc(x float32) float32 {
29 if x == 0 {
30 return 1
31 }
32 return float32(math.Sin(math.Pi*float64(x)) / (math.Pi * float64(x)))
33}
34
35type resamp struct {
36 name string
37 support float32
38 kernel func(float32) float32
39}
40
41func (r resamp) String() string {
42 return r.name
43}
44
45func (r resamp) Support() float32 {
46 return r.support
47}
48
49func (r resamp) Kernel(x float32) float32 {
50 return r.kernel(x)
51}
52
53// NearestNeighborResampling is a nearest neighbor resampling filter.
54var NearestNeighborResampling Resampling
55
56// BoxResampling is a box resampling filter (average of surrounding pixels).
57var BoxResampling Resampling
58
59// LinearResampling is a bilinear resampling filter.
60var LinearResampling Resampling
61
62// CubicResampling is a bicubic resampling filter (Catmull-Rom).
63var CubicResampling Resampling
64
65// LanczosResampling is a Lanczos resampling filter (3 lobes).
66var LanczosResampling Resampling
67
68type resampWeight struct {
69 index int
70 weight float32
71}
72
73func prepareResampWeights(dstSize, srcSize int, resampling Resampling) [][]resampWeight {
74 delta := float32(srcSize) / float32(dstSize)
75 scale := delta
76 if scale < 1 {
77 scale = 1
78 }
79 radius := float32(math.Ceil(float64(scale * resampling.Support())))
80
81 result := make([][]resampWeight, dstSize)
82 tmp := make([]resampWeight, 0, dstSize*int(radius+2)*2)
83
84 for i := 0; i < dstSize; i++ {
85 center := (float32(i)+0.5)*delta - 0.5
86
87 left := int(math.Ceil(float64(center - radius)))
88 if left < 0 {
89 left = 0
90 }
91 right := int(math.Floor(float64(center + radius)))
92 if right > srcSize-1 {
93 right = srcSize - 1
94 }
95
96 var sum float32
97 for j := left; j <= right; j++ {
98 weight := resampling.Kernel((float32(j) - center) / scale)
99 if weight == 0 {
100 continue
101 }
102 tmp = append(tmp, resampWeight{
103 index: j,
104 weight: weight,
105 })
106 sum += weight
107 }
108
109 for j := range tmp {
110 tmp[j].weight /= sum
111 }
112
113 result[i] = tmp
114 tmp = tmp[len(tmp):]
115 }
116
117 return result
118}
119
120func resizeLine(dst []pixel, src []pixel, weights [][]resampWeight) {
121 for i := 0; i < len(dst); i++ {
122 var r, g, b, a float32
123 for _, w := range weights[i] {
124 c := src[w.index]
125 wa := c.a * w.weight
126 r += c.r * wa
127 g += c.g * wa
128 b += c.b * wa
129 a += wa
130 }
131 if a != 0 {
132 r /= a
133 g /= a
134 b /= a
135 }
136 dst[i] = pixel{r, g, b, a}
137 }
138}
139
140func resizeHorizontal(dst draw.Image, src image.Image, w int, resampling Resampling, options *Options) {
141 srcb := src.Bounds()
142 dstb := dst.Bounds()
143
144 weights := prepareResampWeights(w, srcb.Dx(), resampling)
145
146 pixGetter := newPixelGetter(src)
147 pixSetter := newPixelSetter(dst)
148
149 parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
150 srcBuf := make([]pixel, srcb.Dx())
151 dstBuf := make([]pixel, w)
152 for srcy := pmin; srcy < pmax; srcy++ {
153 pixGetter.getPixelRow(srcy, &srcBuf)
154 resizeLine(dstBuf, srcBuf, weights)
155 pixSetter.setPixelRow(dstb.Min.Y+srcy-srcb.Min.Y, dstBuf)
156 }
157 })
158}
159
160func resizeVertical(dst draw.Image, src image.Image, h int, resampling Resampling, options *Options) {
161 srcb := src.Bounds()
162 dstb := dst.Bounds()
163
164 weights := prepareResampWeights(h, srcb.Dy(), resampling)
165
166 pixGetter := newPixelGetter(src)
167 pixSetter := newPixelSetter(dst)
168
169 parallelize(options.Parallelization, srcb.Min.X, srcb.Max.X, func(pmin, pmax int) {
170 srcBuf := make([]pixel, srcb.Dy())
171 dstBuf := make([]pixel, h)
172 for srcx := pmin; srcx < pmax; srcx++ {
173 pixGetter.getPixelColumn(srcx, &srcBuf)
174 resizeLine(dstBuf, srcBuf, weights)
175 pixSetter.setPixelColumn(dstb.Min.X+srcx-srcb.Min.X, dstBuf)
176 }
177 })
178}
179
180func resizeNearest(dst draw.Image, src image.Image, w, h int, options *Options) {
181 srcb := src.Bounds()
182 dstb := dst.Bounds()
183 dx := float64(srcb.Dx()) / float64(w)
184 dy := float64(srcb.Dy()) / float64(h)
185
186 pixGetter := newPixelGetter(src)
187 pixSetter := newPixelSetter(dst)
188
189 parallelize(options.Parallelization, dstb.Min.Y, dstb.Min.Y+h, func(pmin, pmax int) {
190 for dsty := pmin; dsty < pmax; dsty++ {
191 for dstx := dstb.Min.X; dstx < dstb.Min.X+w; dstx++ {
192 fx := math.Floor((float64(dstx-dstb.Min.X) + 0.5) * dx)
193 fy := math.Floor((float64(dsty-dstb.Min.Y) + 0.5) * dy)
194 srcx := srcb.Min.X + int(fx)
195 srcy := srcb.Min.Y + int(fy)
196 px := pixGetter.getPixel(srcx, srcy)
197 pixSetter.setPixel(dstx, dsty, px)
198 }
199 }
200 })
201}
202
203type resizeFilter struct {
204 width int
205 height int
206 resampling Resampling
207}
208
209func (p *resizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
210 w, h := p.width, p.height
211 srcw, srch := srcBounds.Dx(), srcBounds.Dy()
212
213 if (w == 0 && h == 0) || w < 0 || h < 0 || srcw <= 0 || srch <= 0 {
214 dstBounds = image.Rect(0, 0, 0, 0)
215 } else if w == 0 {
216 fw := float64(h) * float64(srcw) / float64(srch)
217 dstw := int(math.Max(1, math.Floor(fw+0.5)))
218 dstBounds = image.Rect(0, 0, dstw, h)
219 } else if h == 0 {
220 fh := float64(w) * float64(srch) / float64(srcw)
221 dsth := int(math.Max(1, math.Floor(fh+0.5)))
222 dstBounds = image.Rect(0, 0, w, dsth)
223 } else {
224 dstBounds = image.Rect(0, 0, w, h)
225 }
226
227 return
228}
229
230func (p *resizeFilter) Draw(dst draw.Image, src image.Image, options *Options) {
231 if options == nil {
232 options = &defaultOptions
233 }
234
235 b := p.Bounds(src.Bounds())
236 w, h := b.Dx(), b.Dy()
237
238 if w <= 0 || h <= 0 {
239 return
240 }
241
242 if src.Bounds().Dx() == w && src.Bounds().Dy() == h {
243 copyimage(dst, src, options)
244 return
245 }
246
247 if p.resampling.Support() <= 0 {
248 resizeNearest(dst, src, w, h, options)
249 return
250 }
251
252 if src.Bounds().Dx() == w {
253 resizeVertical(dst, src, h, p.resampling, options)
254 return
255 }
256
257 if src.Bounds().Dy() == h {
258 resizeHorizontal(dst, src, w, p.resampling, options)
259 return
260 }
261
262 tmp := createTempImage(image.Rect(0, 0, w, src.Bounds().Dy()))
263 resizeHorizontal(tmp, src, w, p.resampling, options)
264 resizeVertical(dst, tmp, h, p.resampling, options)
265}
266
267// Resize creates a filter that resizes an image to the specified width and height using the specified resampling.
268// If one of width or height is 0, the image aspect ratio is preserved.
269// Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling.
270//
271// Example:
272//
273// // Resize the src image to width=300 preserving the aspect ratio.
274// g := gift.New(
275// gift.Resize(300, 0, gift.LanczosResampling),
276// )
277// dst := image.NewRGBA(g.Bounds(src.Bounds()))
278// g.Draw(dst, src)
279//
280func Resize(width, height int, resampling Resampling) Filter {
281 return &resizeFilter{
282 width: width,
283 height: height,
284 resampling: resampling,
285 }
286}
287
288type resizeToFitFilter struct {
289 width int
290 height int
291 resampling Resampling
292}
293
294func (p *resizeToFitFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
295 w, h := p.width, p.height
296 srcw, srch := srcBounds.Dx(), srcBounds.Dy()
297
298 if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 {
299 return image.Rect(0, 0, 0, 0)
300 }
301
302 if srcw <= w && srch <= h {
303 return image.Rect(0, 0, srcw, srch)
304 }
305
306 wratio := float64(srcw) / float64(w)
307 hratio := float64(srch) / float64(h)
308
309 var dstw, dsth int
310 if wratio > hratio {
311 dstw = w
312 dsth = minint(int(float64(srch)/wratio+0.5), h)
313 } else {
314 dsth = h
315 dstw = minint(int(float64(srcw)/hratio+0.5), w)
316 }
317
318 return image.Rect(0, 0, dstw, dsth)
319}
320
321func (p *resizeToFitFilter) Draw(dst draw.Image, src image.Image, options *Options) {
322 b := p.Bounds(src.Bounds())
323 Resize(b.Dx(), b.Dy(), p.resampling).Draw(dst, src, options)
324}
325
326// ResizeToFit creates a filter that resizes an image to fit within the specified dimensions while preserving the aspect ratio.
327// Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling.
328func ResizeToFit(width, height int, resampling Resampling) Filter {
329 return &resizeToFitFilter{
330 width: width,
331 height: height,
332 resampling: resampling,
333 }
334}
335
336type resizeToFillFilter struct {
337 width int
338 height int
339 anchor Anchor
340 resampling Resampling
341}
342
343func (p *resizeToFillFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
344 w, h := p.width, p.height
345 srcw, srch := srcBounds.Dx(), srcBounds.Dy()
346
347 if w <= 0 || h <= 0 || srcw <= 0 || srch <= 0 {
348 return image.Rect(0, 0, 0, 0)
349 }
350
351 return image.Rect(0, 0, w, h)
352}
353
354func (p *resizeToFillFilter) Draw(dst draw.Image, src image.Image, options *Options) {
355 b := p.Bounds(src.Bounds())
356 w, h := b.Dx(), b.Dy()
357
358 if w <= 0 || h <= 0 {
359 return
360 }
361
362 srcw, srch := src.Bounds().Dx(), src.Bounds().Dy()
363
364 wratio := float64(srcw) / float64(w)
365 hratio := float64(srch) / float64(h)
366
367 var tmpw, tmph int
368 if wratio < hratio {
369 tmpw = w
370 tmph = maxint(int(float64(srch)/wratio+0.5), h)
371 } else {
372 tmph = h
373 tmpw = maxint(int(float64(srcw)/hratio+0.5), w)
374 }
375
376 tmp := createTempImage(image.Rect(0, 0, tmpw, tmph))
377 Resize(tmpw, tmph, p.resampling).Draw(tmp, src, options)
378 CropToSize(w, h, p.anchor).Draw(dst, tmp, options)
379}
380
381// ResizeToFill creates a filter that resizes an image to the smallest possible size that will cover the specified dimensions,
382// then crops the resized image to the specified dimensions using the specified anchor point.
383// Supported resampling parameters: NearestNeighborResampling, BoxResampling, LinearResampling, CubicResampling, LanczosResampling.
384func ResizeToFill(width, height int, resampling Resampling, anchor Anchor) Filter {
385 return &resizeToFillFilter{
386 width: width,
387 height: height,
388 anchor: anchor,
389 resampling: resampling,
390 }
391}
392
393func init() {
394 // Nearest neighbor resampling filter.
395 NearestNeighborResampling = resamp{
396 name: "NearestNeighborResampling",
397 support: 0,
398 kernel: func(x float32) float32 {
399 return 0
400 },
401 }
402
403 // Box resampling filter.
404 BoxResampling = resamp{
405 name: "BoxResampling",
406 support: 0.5,
407 kernel: func(x float32) float32 {
408 if x < 0 {
409 x = -x
410 }
411 if x <= 0.5 {
412 return 1
413 }
414 return 0
415 },
416 }
417
418 // Linear resampling filter.
419 LinearResampling = resamp{
420 name: "LinearResampling",
421 support: 1,
422 kernel: func(x float32) float32 {
423 if x < 0 {
424 x = -x
425 }
426 if x < 1 {
427 return 1 - x
428 }
429 return 0
430 },
431 }
432
433 // Cubic resampling filter (Catmull-Rom).
434 CubicResampling = resamp{
435 name: "CubicResampling",
436 support: 2,
437 kernel: func(x float32) float32 {
438 if x < 0 {
439 x = -x
440 }
441 if x < 2 {
442 return bcspline(x, 0, 0.5)
443 }
444 return 0
445 },
446 }
447
448 // Lanczos resampling filter (3 lobes).
449 LanczosResampling = resamp{
450 name: "LanczosResampling",
451 support: 3,
452 kernel: func(x float32) float32 {
453 if x < 0 {
454 x = -x
455 }
456 if x < 3 {
457 return sinc(x) * sinc(x/3)
458 }
459 return 0
460 },
461 }
462}