1package imaging
2
3import (
4	"image"
5	"math"
6)
7
8type indexWeight struct {
9	index  int
10	weight float64
11}
12
13func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWeight {
14	du := float64(srcSize) / float64(dstSize)
15	scale := du
16	if scale < 1.0 {
17		scale = 1.0
18	}
19	ru := math.Ceil(scale * filter.Support)
20
21	out := make([][]indexWeight, dstSize)
22	tmp := make([]indexWeight, 0, dstSize*int(ru+2)*2)
23
24	for v := 0; v < dstSize; v++ {
25		fu := (float64(v)+0.5)*du - 0.5
26
27		begin := int(math.Ceil(fu - ru))
28		if begin < 0 {
29			begin = 0
30		}
31		end := int(math.Floor(fu + ru))
32		if end > srcSize-1 {
33			end = srcSize - 1
34		}
35
36		var sum float64
37		for u := begin; u <= end; u++ {
38			w := filter.Kernel((float64(u) - fu) / scale)
39			if w != 0 {
40				sum += w
41				tmp = append(tmp, indexWeight{index: u, weight: w})
42			}
43		}
44		if sum != 0 {
45			for i := range tmp {
46				tmp[i].weight /= sum
47			}
48		}
49
50		out[v] = tmp
51		tmp = tmp[len(tmp):]
52	}
53
54	return out
55}
56
57// Resize resizes the image to the specified width and height using the specified resampling
58// filter and returns the transformed image. If one of width or height is 0, the image aspect
59// ratio is preserved.
60//
61// Example:
62//
63//	dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
64//
65func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
66	dstW, dstH := width, height
67	if dstW < 0 || dstH < 0 {
68		return &image.NRGBA{}
69	}
70	if dstW == 0 && dstH == 0 {
71		return &image.NRGBA{}
72	}
73
74	srcW := img.Bounds().Dx()
75	srcH := img.Bounds().Dy()
76	if srcW <= 0 || srcH <= 0 {
77		return &image.NRGBA{}
78	}
79
80	// If new width or height is 0 then preserve aspect ratio, minimum 1px.
81	if dstW == 0 {
82		tmpW := float64(dstH) * float64(srcW) / float64(srcH)
83		dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
84	}
85	if dstH == 0 {
86		tmpH := float64(dstW) * float64(srcH) / float64(srcW)
87		dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
88	}
89
90	if filter.Support <= 0 {
91		// Nearest-neighbor special case.
92		return resizeNearest(img, dstW, dstH)
93	}
94
95	if srcW != dstW && srcH != dstH {
96		return resizeVertical(resizeHorizontal(img, dstW, filter), dstH, filter)
97	}
98	if srcW != dstW {
99		return resizeHorizontal(img, dstW, filter)
100	}
101	if srcH != dstH {
102		return resizeVertical(img, dstH, filter)
103	}
104	return Clone(img)
105}
106
107func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image.NRGBA {
108	src := newScanner(img)
109	dst := image.NewNRGBA(image.Rect(0, 0, width, src.h))
110	weights := precomputeWeights(width, src.w, filter)
111	parallel(0, src.h, func(ys <-chan int) {
112		scanLine := make([]uint8, src.w*4)
113		for y := range ys {
114			src.scan(0, y, src.w, y+1, scanLine)
115			j0 := y * dst.Stride
116			for x := range weights {
117				var r, g, b, a float64
118				for _, w := range weights[x] {
119					i := w.index * 4
120					s := scanLine[i : i+4 : i+4]
121					aw := float64(s[3]) * w.weight
122					r += float64(s[0]) * aw
123					g += float64(s[1]) * aw
124					b += float64(s[2]) * aw
125					a += aw
126				}
127				if a != 0 {
128					aInv := 1 / a
129					j := j0 + x*4
130					d := dst.Pix[j : j+4 : j+4]
131					d[0] = clamp(r * aInv)
132					d[1] = clamp(g * aInv)
133					d[2] = clamp(b * aInv)
134					d[3] = clamp(a)
135				}
136			}
137		}
138	})
139	return dst
140}
141
142func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.NRGBA {
143	src := newScanner(img)
144	dst := image.NewNRGBA(image.Rect(0, 0, src.w, height))
145	weights := precomputeWeights(height, src.h, filter)
146	parallel(0, src.w, func(xs <-chan int) {
147		scanLine := make([]uint8, src.h*4)
148		for x := range xs {
149			src.scan(x, 0, x+1, src.h, scanLine)
150			for y := range weights {
151				var r, g, b, a float64
152				for _, w := range weights[y] {
153					i := w.index * 4
154					s := scanLine[i : i+4 : i+4]
155					aw := float64(s[3]) * w.weight
156					r += float64(s[0]) * aw
157					g += float64(s[1]) * aw
158					b += float64(s[2]) * aw
159					a += aw
160				}
161				if a != 0 {
162					aInv := 1 / a
163					j := y*dst.Stride + x*4
164					d := dst.Pix[j : j+4 : j+4]
165					d[0] = clamp(r * aInv)
166					d[1] = clamp(g * aInv)
167					d[2] = clamp(b * aInv)
168					d[3] = clamp(a)
169				}
170			}
171		}
172	})
173	return dst
174}
175
176// resizeNearest is a fast nearest-neighbor resize, no filtering.
177func resizeNearest(img image.Image, width, height int) *image.NRGBA {
178	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
179	dx := float64(img.Bounds().Dx()) / float64(width)
180	dy := float64(img.Bounds().Dy()) / float64(height)
181
182	if dx > 1 && dy > 1 {
183		src := newScanner(img)
184		parallel(0, height, func(ys <-chan int) {
185			for y := range ys {
186				srcY := int((float64(y) + 0.5) * dy)
187				dstOff := y * dst.Stride
188				for x := 0; x < width; x++ {
189					srcX := int((float64(x) + 0.5) * dx)
190					src.scan(srcX, srcY, srcX+1, srcY+1, dst.Pix[dstOff:dstOff+4])
191					dstOff += 4
192				}
193			}
194		})
195	} else {
196		src := toNRGBA(img)
197		parallel(0, height, func(ys <-chan int) {
198			for y := range ys {
199				srcY := int((float64(y) + 0.5) * dy)
200				srcOff0 := srcY * src.Stride
201				dstOff := y * dst.Stride
202				for x := 0; x < width; x++ {
203					srcX := int((float64(x) + 0.5) * dx)
204					srcOff := srcOff0 + srcX*4
205					copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
206					dstOff += 4
207				}
208			}
209		})
210	}
211
212	return dst
213}
214
215// Fit scales down the image using the specified resample filter to fit the specified
216// maximum width and height and returns the transformed image.
217//
218// Example:
219//
220//	dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
221//
222func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
223	maxW, maxH := width, height
224
225	if maxW <= 0 || maxH <= 0 {
226		return &image.NRGBA{}
227	}
228
229	srcBounds := img.Bounds()
230	srcW := srcBounds.Dx()
231	srcH := srcBounds.Dy()
232
233	if srcW <= 0 || srcH <= 0 {
234		return &image.NRGBA{}
235	}
236
237	if srcW <= maxW && srcH <= maxH {
238		return Clone(img)
239	}
240
241	srcAspectRatio := float64(srcW) / float64(srcH)
242	maxAspectRatio := float64(maxW) / float64(maxH)
243
244	var newW, newH int
245	if srcAspectRatio > maxAspectRatio {
246		newW = maxW
247		newH = int(float64(newW) / srcAspectRatio)
248	} else {
249		newH = maxH
250		newW = int(float64(newH) * srcAspectRatio)
251	}
252
253	return Resize(img, newW, newH, filter)
254}
255
256// Fill creates an image with the specified dimensions and fills it with the scaled source image.
257// To achieve the correct aspect ratio without stretching, the source image will be cropped.
258//
259// Example:
260//
261//	dstImage := imaging.Fill(srcImage, 800, 600, imaging.Center, imaging.Lanczos)
262//
263func Fill(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
264	dstW, dstH := width, height
265
266	if dstW <= 0 || dstH <= 0 {
267		return &image.NRGBA{}
268	}
269
270	srcBounds := img.Bounds()
271	srcW := srcBounds.Dx()
272	srcH := srcBounds.Dy()
273
274	if srcW <= 0 || srcH <= 0 {
275		return &image.NRGBA{}
276	}
277
278	if srcW == dstW && srcH == dstH {
279		return Clone(img)
280	}
281
282	if srcW >= 100 && srcH >= 100 {
283		return cropAndResize(img, dstW, dstH, anchor, filter)
284	}
285	return resizeAndCrop(img, dstW, dstH, anchor, filter)
286}
287
288// cropAndResize crops the image to the smallest possible size that has the required aspect ratio using
289// the given anchor point, then scales it to the specified dimensions and returns the transformed image.
290//
291// This is generally faster than resizing first, but may result in inaccuracies when used on small source images.
292func cropAndResize(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
293	dstW, dstH := width, height
294
295	srcBounds := img.Bounds()
296	srcW := srcBounds.Dx()
297	srcH := srcBounds.Dy()
298	srcAspectRatio := float64(srcW) / float64(srcH)
299	dstAspectRatio := float64(dstW) / float64(dstH)
300
301	var tmp *image.NRGBA
302	if srcAspectRatio < dstAspectRatio {
303		cropH := float64(srcW) * float64(dstH) / float64(dstW)
304		tmp = CropAnchor(img, srcW, int(math.Max(1, cropH)+0.5), anchor)
305	} else {
306		cropW := float64(srcH) * float64(dstW) / float64(dstH)
307		tmp = CropAnchor(img, int(math.Max(1, cropW)+0.5), srcH, anchor)
308	}
309
310	return Resize(tmp, dstW, dstH, filter)
311}
312
313// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions,
314// crops the resized image to the specified dimensions using the given anchor point and returns
315// the transformed image.
316func resizeAndCrop(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
317	dstW, dstH := width, height
318
319	srcBounds := img.Bounds()
320	srcW := srcBounds.Dx()
321	srcH := srcBounds.Dy()
322	srcAspectRatio := float64(srcW) / float64(srcH)
323	dstAspectRatio := float64(dstW) / float64(dstH)
324
325	var tmp *image.NRGBA
326	if srcAspectRatio < dstAspectRatio {
327		tmp = Resize(img, dstW, 0, filter)
328	} else {
329		tmp = Resize(img, 0, dstH, filter)
330	}
331
332	return CropAnchor(tmp, dstW, dstH, anchor)
333}
334
335// Thumbnail scales the image up or down using the specified resample filter, crops it
336// to the specified width and hight and returns the transformed image.
337//
338// Example:
339//
340//	dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
341//
342func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
343	return Fill(img, width, height, Center, filter)
344}
345
346// ResampleFilter specifies a resampling filter to be used for image resizing.
347//
348//	General filter recommendations:
349//
350//	- Lanczos
351//		A high-quality resampling filter for photographic images yielding sharp results.
352//
353//	- CatmullRom
354//		A sharp cubic filter that is faster than Lanczos filter while providing similar results.
355//
356//	- MitchellNetravali
357//		A cubic filter that produces smoother results with less ringing artifacts than CatmullRom.
358//
359//	- Linear
360//		Bilinear resampling filter, produces a smooth output. Faster than cubic filters.
361//
362//	- Box
363//		Simple and fast averaging filter appropriate for downscaling.
364//		When upscaling it's similar to NearestNeighbor.
365//
366//	- NearestNeighbor
367//		Fastest resampling filter, no antialiasing.
368//
369type ResampleFilter struct {
370	Support float64
371	Kernel  func(float64) float64
372}
373
374// NearestNeighbor is a nearest-neighbor filter (no anti-aliasing).
375var NearestNeighbor ResampleFilter
376
377// Box filter (averaging pixels).
378var Box ResampleFilter
379
380// Linear filter.
381var Linear ResampleFilter
382
383// Hermite cubic spline filter (BC-spline; B=0; C=0).
384var Hermite ResampleFilter
385
386// MitchellNetravali is Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
387var MitchellNetravali ResampleFilter
388
389// CatmullRom is a Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
390var CatmullRom ResampleFilter
391
392// BSpline is a smooth cubic filter (BC-spline; B=1; C=0).
393var BSpline ResampleFilter
394
395// Gaussian is a Gaussian blurring filter.
396var Gaussian ResampleFilter
397
398// Bartlett is a Bartlett-windowed sinc filter (3 lobes).
399var Bartlett ResampleFilter
400
401// Lanczos filter (3 lobes).
402var Lanczos ResampleFilter
403
404// Hann is a Hann-windowed sinc filter (3 lobes).
405var Hann ResampleFilter
406
407// Hamming is a Hamming-windowed sinc filter (3 lobes).
408var Hamming ResampleFilter
409
410// Blackman is a Blackman-windowed sinc filter (3 lobes).
411var Blackman ResampleFilter
412
413// Welch is a Welch-windowed sinc filter (parabolic window, 3 lobes).
414var Welch ResampleFilter
415
416// Cosine is a Cosine-windowed sinc filter (3 lobes).
417var Cosine ResampleFilter
418
419func bcspline(x, b, c float64) float64 {
420	var y float64
421	x = math.Abs(x)
422	if x < 1.0 {
423		y = ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
424	} else if x < 2.0 {
425		y = ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
426	}
427	return y
428}
429
430func sinc(x float64) float64 {
431	if x == 0 {
432		return 1
433	}
434	return math.Sin(math.Pi*x) / (math.Pi * x)
435}
436
437func init() {
438	NearestNeighbor = ResampleFilter{
439		Support: 0.0, // special case - not applying the filter
440	}
441
442	Box = ResampleFilter{
443		Support: 0.5,
444		Kernel: func(x float64) float64 {
445			x = math.Abs(x)
446			if x <= 0.5 {
447				return 1.0
448			}
449			return 0
450		},
451	}
452
453	Linear = ResampleFilter{
454		Support: 1.0,
455		Kernel: func(x float64) float64 {
456			x = math.Abs(x)
457			if x < 1.0 {
458				return 1.0 - x
459			}
460			return 0
461		},
462	}
463
464	Hermite = ResampleFilter{
465		Support: 1.0,
466		Kernel: func(x float64) float64 {
467			x = math.Abs(x)
468			if x < 1.0 {
469				return bcspline(x, 0.0, 0.0)
470			}
471			return 0
472		},
473	}
474
475	MitchellNetravali = ResampleFilter{
476		Support: 2.0,
477		Kernel: func(x float64) float64 {
478			x = math.Abs(x)
479			if x < 2.0 {
480				return bcspline(x, 1.0/3.0, 1.0/3.0)
481			}
482			return 0
483		},
484	}
485
486	CatmullRom = ResampleFilter{
487		Support: 2.0,
488		Kernel: func(x float64) float64 {
489			x = math.Abs(x)
490			if x < 2.0 {
491				return bcspline(x, 0.0, 0.5)
492			}
493			return 0
494		},
495	}
496
497	BSpline = ResampleFilter{
498		Support: 2.0,
499		Kernel: func(x float64) float64 {
500			x = math.Abs(x)
501			if x < 2.0 {
502				return bcspline(x, 1.0, 0.0)
503			}
504			return 0
505		},
506	}
507
508	Gaussian = ResampleFilter{
509		Support: 2.0,
510		Kernel: func(x float64) float64 {
511			x = math.Abs(x)
512			if x < 2.0 {
513				return math.Exp(-2 * x * x)
514			}
515			return 0
516		},
517	}
518
519	Bartlett = ResampleFilter{
520		Support: 3.0,
521		Kernel: func(x float64) float64 {
522			x = math.Abs(x)
523			if x < 3.0 {
524				return sinc(x) * (3.0 - x) / 3.0
525			}
526			return 0
527		},
528	}
529
530	Lanczos = ResampleFilter{
531		Support: 3.0,
532		Kernel: func(x float64) float64 {
533			x = math.Abs(x)
534			if x < 3.0 {
535				return sinc(x) * sinc(x/3.0)
536			}
537			return 0
538		},
539	}
540
541	Hann = ResampleFilter{
542		Support: 3.0,
543		Kernel: func(x float64) float64 {
544			x = math.Abs(x)
545			if x < 3.0 {
546				return sinc(x) * (0.5 + 0.5*math.Cos(math.Pi*x/3.0))
547			}
548			return 0
549		},
550	}
551
552	Hamming = ResampleFilter{
553		Support: 3.0,
554		Kernel: func(x float64) float64 {
555			x = math.Abs(x)
556			if x < 3.0 {
557				return sinc(x) * (0.54 + 0.46*math.Cos(math.Pi*x/3.0))
558			}
559			return 0
560		},
561	}
562
563	Blackman = ResampleFilter{
564		Support: 3.0,
565		Kernel: func(x float64) float64 {
566			x = math.Abs(x)
567			if x < 3.0 {
568				return sinc(x) * (0.42 - 0.5*math.Cos(math.Pi*x/3.0+math.Pi) + 0.08*math.Cos(2.0*math.Pi*x/3.0))
569			}
570			return 0
571		},
572	}
573
574	Welch = ResampleFilter{
575		Support: 3.0,
576		Kernel: func(x float64) float64 {
577			x = math.Abs(x)
578			if x < 3.0 {
579				return sinc(x) * (1.0 - (x * x / 9.0))
580			}
581			return 0
582		},
583	}
584
585	Cosine = ResampleFilter{
586		Support: 3.0,
587		Kernel: func(x float64) float64 {
588			x = math.Abs(x)
589			if x < 3.0 {
590				return sinc(x) * math.Cos((math.Pi/2.0)*(x/3.0))
591			}
592			return 0
593		},
594	}
595}
596