resize.go

  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}