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}