1package gift
2
3import (
4 "image"
5 "image/color"
6 "image/draw"
7)
8
9type transformType int
10
11const (
12 ttRotate90 transformType = iota
13 ttRotate180
14 ttRotate270
15 ttFlipHorizontal
16 ttFlipVertical
17 ttTranspose
18 ttTransverse
19)
20
21type transformFilter struct {
22 tt transformType
23}
24
25func (p *transformFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
26 if p.tt == ttRotate90 || p.tt == ttRotate270 || p.tt == ttTranspose || p.tt == ttTransverse {
27 dstBounds = image.Rect(0, 0, srcBounds.Dy(), srcBounds.Dx())
28 } else {
29 dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
30 }
31 return
32}
33
34func (p *transformFilter) Draw(dst draw.Image, src image.Image, options *Options) {
35 if options == nil {
36 options = &defaultOptions
37 }
38
39 srcb := src.Bounds()
40 dstb := dst.Bounds()
41
42 pixGetter := newPixelGetter(src)
43 pixSetter := newPixelSetter(dst)
44
45 parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
46 for srcy := pmin; srcy < pmax; srcy++ {
47 for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ {
48 var dstx, dsty int
49 switch p.tt {
50 case ttRotate90:
51 dstx = dstb.Min.X + srcy - srcb.Min.Y
52 dsty = dstb.Min.Y + srcb.Max.X - srcx - 1
53 case ttRotate180:
54 dstx = dstb.Min.X + srcb.Max.X - srcx - 1
55 dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1
56 case ttRotate270:
57 dstx = dstb.Min.X + srcb.Max.Y - srcy - 1
58 dsty = dstb.Min.Y + srcx - srcb.Min.X
59 case ttFlipHorizontal:
60 dstx = dstb.Min.X + srcb.Max.X - srcx - 1
61 dsty = dstb.Min.Y + srcy - srcb.Min.Y
62 case ttFlipVertical:
63 dstx = dstb.Min.X + srcx - srcb.Min.X
64 dsty = dstb.Min.Y + srcb.Max.Y - srcy - 1
65 case ttTranspose:
66 dstx = dstb.Min.X + srcy - srcb.Min.Y
67 dsty = dstb.Min.Y + srcx - srcb.Min.X
68 case ttTransverse:
69 dstx = dstb.Min.Y + srcb.Max.Y - srcy - 1
70 dsty = dstb.Min.X + srcb.Max.X - srcx - 1
71 }
72 pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy))
73 }
74 }
75 })
76}
77
78// Rotate90 creates a filter that rotates an image 90 degrees counter-clockwise.
79func Rotate90() Filter {
80 return &transformFilter{
81 tt: ttRotate90,
82 }
83}
84
85// Rotate180 creates a filter that rotates an image 180 degrees counter-clockwise.
86func Rotate180() Filter {
87 return &transformFilter{
88 tt: ttRotate180,
89 }
90}
91
92// Rotate270 creates a filter that rotates an image 270 degrees counter-clockwise.
93func Rotate270() Filter {
94 return &transformFilter{
95 tt: ttRotate270,
96 }
97}
98
99// FlipHorizontal creates a filter that flips an image horizontally.
100func FlipHorizontal() Filter {
101 return &transformFilter{
102 tt: ttFlipHorizontal,
103 }
104}
105
106// FlipVertical creates a filter that flips an image vertically.
107func FlipVertical() Filter {
108 return &transformFilter{
109 tt: ttFlipVertical,
110 }
111}
112
113// Transpose creates a filter that flips an image horizontally and rotates 90 degrees counter-clockwise.
114func Transpose() Filter {
115 return &transformFilter{
116 tt: ttTranspose,
117 }
118}
119
120// Transverse creates a filter that flips an image vertically and rotates 90 degrees counter-clockwise.
121func Transverse() Filter {
122 return &transformFilter{
123 tt: ttTransverse,
124 }
125}
126
127// Interpolation is an interpolation algorithm used for image transformation.
128type Interpolation int
129
130const (
131 // NearestNeighborInterpolation is a nearest-neighbor interpolation algorithm.
132 NearestNeighborInterpolation Interpolation = iota
133 // LinearInterpolation is a bilinear interpolation algorithm.
134 LinearInterpolation
135 // CubicInterpolation is a bicubic interpolation algorithm.
136 CubicInterpolation
137)
138
139func rotatePoint(x, y, asin, acos float32) (float32, float32) {
140 newx := x*acos - y*asin
141 newy := x*asin + y*acos
142 return newx, newy
143}
144
145func calcRotatedSize(w, h int, angle float32) (int, int) {
146 if w <= 0 || h <= 0 {
147 return 0, 0
148 }
149
150 xoff := float32(w)/2 - 0.5
151 yoff := float32(h)/2 - 0.5
152
153 asin, acos := sincosf32(angle)
154 x1, y1 := rotatePoint(0-xoff, 0-yoff, asin, acos)
155 x2, y2 := rotatePoint(float32(w-1)-xoff, 0-yoff, asin, acos)
156 x3, y3 := rotatePoint(float32(w-1)-xoff, float32(h-1)-yoff, asin, acos)
157 x4, y4 := rotatePoint(0-xoff, float32(h-1)-yoff, asin, acos)
158
159 minx := minf32(x1, minf32(x2, minf32(x3, x4)))
160 maxx := maxf32(x1, maxf32(x2, maxf32(x3, x4)))
161 miny := minf32(y1, minf32(y2, minf32(y3, y4)))
162 maxy := maxf32(y1, maxf32(y2, maxf32(y3, y4)))
163
164 neww := maxx - minx + 1
165 if neww-floorf32(neww) > 0.01 {
166 neww += 2
167 }
168 newh := maxy - miny + 1
169 if newh-floorf32(newh) > 0.01 {
170 newh += 2
171 }
172 return int(neww), int(newh)
173}
174
175type rotateFilter struct {
176 angle float32
177 bgcolor color.Color
178 interpolation Interpolation
179}
180
181func (p *rotateFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
182 w, h := calcRotatedSize(srcBounds.Dx(), srcBounds.Dy(), p.angle)
183 dstBounds = image.Rect(0, 0, w, h)
184 return
185}
186
187func (p *rotateFilter) Draw(dst draw.Image, src image.Image, options *Options) {
188 if options == nil {
189 options = &defaultOptions
190 }
191
192 srcb := src.Bounds()
193 dstb := dst.Bounds()
194
195 w, h := calcRotatedSize(srcb.Dx(), srcb.Dy(), p.angle)
196 if w <= 0 || h <= 0 {
197 return
198 }
199
200 srcxoff := float32(srcb.Dx())/2 - 0.5
201 srcyoff := float32(srcb.Dy())/2 - 0.5
202 dstxoff := float32(w)/2 - 0.5
203 dstyoff := float32(h)/2 - 0.5
204
205 bgpx := pixelFromColor(p.bgcolor)
206 asin, acos := sincosf32(p.angle)
207
208 pixGetter := newPixelGetter(src)
209 pixSetter := newPixelSetter(dst)
210
211 parallelize(options.Parallelization, 0, h, func(pmin, pmax int) {
212 for y := pmin; y < pmax; y++ {
213 for x := 0; x < w; x++ {
214
215 xf, yf := rotatePoint(float32(x)-dstxoff, float32(y)-dstyoff, asin, acos)
216 xf, yf = float32(srcb.Min.X)+xf+srcxoff, float32(srcb.Min.Y)+yf+srcyoff
217 var px pixel
218
219 switch p.interpolation {
220 case CubicInterpolation:
221 px = interpolateCubic(xf, yf, srcb, pixGetter, bgpx)
222 case LinearInterpolation:
223 px = interpolateLinear(xf, yf, srcb, pixGetter, bgpx)
224 default:
225 px = interpolateNearest(xf, yf, srcb, pixGetter, bgpx)
226 }
227
228 pixSetter.setPixel(dstb.Min.X+x, dstb.Min.Y+y, px)
229 }
230 }
231 })
232}
233
234func interpolateCubic(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
235 var pxs [16]pixel
236 var cfs [16]float32
237 var px pixel
238
239 x0, y0 := int(floorf32(xf)), int(floorf32(yf))
240 if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
241 return bgpx
242 }
243 xq, yq := xf-float32(x0), yf-float32(y0)
244
245 for i := 0; i < 4; i++ {
246 for j := 0; j < 4; j++ {
247 pt := image.Pt(x0+j-1, y0+i-1)
248 if pt.In(bounds) {
249 pxs[i*4+j] = pixGetter.getPixel(pt.X, pt.Y)
250 } else {
251 pxs[i*4+j] = bgpx
252 }
253 }
254 }
255
256 const (
257 k04 = 1 / 4.0
258 k12 = 1 / 12.0
259 k36 = 1 / 36.0
260 )
261
262 cfs[0] = k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2)
263 cfs[1] = -k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2)
264 cfs[2] = k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2)
265 cfs[3] = -k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2)
266 cfs[4] = -k12 * xq * (xq - 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1)
267 cfs[5] = k04 * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1)
268 cfs[6] = -k04 * xq * (xq + 1) * (xq - 2) * (yq - 1) * (yq - 2) * (yq + 1)
269 cfs[7] = k12 * xq * (xq - 1) * (xq + 1) * (yq - 1) * (yq - 2) * (yq + 1)
270 cfs[8] = k12 * xq * yq * (xq - 1) * (xq - 2) * (yq + 1) * (yq - 2)
271 cfs[9] = -k04 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq + 1) * (yq - 2)
272 cfs[10] = k04 * xq * yq * (xq + 1) * (xq - 2) * (yq + 1) * (yq - 2)
273 cfs[11] = -k12 * xq * yq * (xq - 1) * (xq + 1) * (yq + 1) * (yq - 2)
274 cfs[12] = -k36 * xq * yq * (xq - 1) * (xq - 2) * (yq - 1) * (yq + 1)
275 cfs[13] = k12 * yq * (xq - 1) * (xq - 2) * (xq + 1) * (yq - 1) * (yq + 1)
276 cfs[14] = -k12 * xq * yq * (xq + 1) * (xq - 2) * (yq - 1) * (yq + 1)
277 cfs[15] = k36 * xq * yq * (xq - 1) * (xq + 1) * (yq - 1) * (yq + 1)
278
279 for i := range pxs {
280 wa := pxs[i].a * cfs[i]
281 px.r += pxs[i].r * wa
282 px.g += pxs[i].g * wa
283 px.b += pxs[i].b * wa
284 px.a += wa
285 }
286
287 if px.a != 0 {
288 px.r /= px.a
289 px.g /= px.a
290 px.b /= px.a
291 }
292
293 return px
294}
295
296func interpolateLinear(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
297 var pxs [4]pixel
298 var cfs [4]float32
299 var px pixel
300
301 x0, y0 := int(floorf32(xf)), int(floorf32(yf))
302 if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
303 return bgpx
304 }
305 xq, yq := xf-float32(x0), yf-float32(y0)
306
307 for i := 0; i < 2; i++ {
308 for j := 0; j < 2; j++ {
309 pt := image.Pt(x0+j, y0+i)
310 if pt.In(bounds) {
311 pxs[i*2+j] = pixGetter.getPixel(pt.X, pt.Y)
312 } else {
313 pxs[i*2+j] = bgpx
314 }
315 }
316 }
317
318 cfs[0] = (1 - xq) * (1 - yq)
319 cfs[1] = xq * (1 - yq)
320 cfs[2] = (1 - xq) * yq
321 cfs[3] = xq * yq
322
323 for i := range pxs {
324 wa := pxs[i].a * cfs[i]
325 px.r += pxs[i].r * wa
326 px.g += pxs[i].g * wa
327 px.b += pxs[i].b * wa
328 px.a += wa
329 }
330
331 if px.a != 0 {
332 px.r /= px.a
333 px.g /= px.a
334 px.b /= px.a
335 }
336
337 return px
338}
339
340func interpolateNearest(xf, yf float32, bounds image.Rectangle, pixGetter *pixelGetter, bgpx pixel) pixel {
341 x0, y0 := int(floorf32(xf+0.5)), int(floorf32(yf+0.5))
342 if image.Pt(x0, y0).In(bounds) {
343 return pixGetter.getPixel(x0, y0)
344 }
345 return bgpx
346}
347
348// Rotate creates a filter that rotates an image by the given angle counter-clockwise.
349// The angle parameter is the rotation angle in degrees.
350// The backgroundColor parameter specifies the color of the uncovered zone after the rotation.
351// The interpolation parameter specifies the interpolation method.
352// Supported interpolation methods: NearestNeighborInterpolation, LinearInterpolation, CubicInterpolation.
353//
354// Example:
355//
356// g := gift.New(
357// gift.Rotate(45, color.Black, gift.LinearInterpolation),
358// )
359// dst := image.NewRGBA(g.Bounds(src.Bounds()))
360// g.Draw(dst, src)
361//
362func Rotate(angle float32, backgroundColor color.Color, interpolation Interpolation) Filter {
363 return &rotateFilter{
364 angle: angle,
365 bgcolor: backgroundColor,
366 interpolation: interpolation,
367 }
368}
369
370type cropFilter struct {
371 rect image.Rectangle
372}
373
374func (p *cropFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
375 b := srcBounds.Intersect(p.rect)
376 return b.Sub(b.Min)
377}
378
379func (p *cropFilter) Draw(dst draw.Image, src image.Image, options *Options) {
380 if options == nil {
381 options = &defaultOptions
382 }
383
384 srcb := src.Bounds().Intersect(p.rect)
385 dstb := dst.Bounds()
386 pixGetter := newPixelGetter(src)
387 pixSetter := newPixelSetter(dst)
388
389 parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
390 for srcy := pmin; srcy < pmax; srcy++ {
391 for srcx := srcb.Min.X; srcx < srcb.Max.X; srcx++ {
392 dstx := dstb.Min.X + srcx - srcb.Min.X
393 dsty := dstb.Min.Y + srcy - srcb.Min.Y
394 pixSetter.setPixel(dstx, dsty, pixGetter.getPixel(srcx, srcy))
395 }
396 }
397 })
398}
399
400// Crop creates a filter that crops the specified rectangular region from an image.
401//
402// Example:
403//
404// g := gift.New(
405// gift.Crop(image.Rect(100, 100, 200, 200)),
406// )
407// dst := image.NewRGBA(g.Bounds(src.Bounds()))
408// g.Draw(dst, src)
409//
410func Crop(rect image.Rectangle) Filter {
411 return &cropFilter{
412 rect: rect,
413 }
414}
415
416// Anchor is the anchor point for image cropping.
417type Anchor int
418
419// Anchor point positions.
420const (
421 CenterAnchor Anchor = iota
422 TopLeftAnchor
423 TopAnchor
424 TopRightAnchor
425 LeftAnchor
426 RightAnchor
427 BottomLeftAnchor
428 BottomAnchor
429 BottomRightAnchor
430)
431
432func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
433 var x, y int
434 switch anchor {
435 case TopLeftAnchor:
436 x = b.Min.X
437 y = b.Min.Y
438 case TopAnchor:
439 x = b.Min.X + (b.Dx()-w)/2
440 y = b.Min.Y
441 case TopRightAnchor:
442 x = b.Max.X - w
443 y = b.Min.Y
444 case LeftAnchor:
445 x = b.Min.X
446 y = b.Min.Y + (b.Dy()-h)/2
447 case RightAnchor:
448 x = b.Max.X - w
449 y = b.Min.Y + (b.Dy()-h)/2
450 case BottomLeftAnchor:
451 x = b.Min.X
452 y = b.Max.Y - h
453 case BottomAnchor:
454 x = b.Min.X + (b.Dx()-w)/2
455 y = b.Max.Y - h
456 case BottomRightAnchor:
457 x = b.Max.X - w
458 y = b.Max.Y - h
459 default:
460 x = b.Min.X + (b.Dx()-w)/2
461 y = b.Min.Y + (b.Dy()-h)/2
462 }
463 return image.Pt(x, y)
464}
465
466type cropToSizeFilter struct {
467 w, h int
468 anchor Anchor
469}
470
471func (p *cropToSizeFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
472 if p.w <= 0 || p.h <= 0 {
473 return image.Rect(0, 0, 0, 0)
474 }
475 pt := anchorPt(srcBounds, p.w, p.h, p.anchor)
476 r := image.Rect(0, 0, p.w, p.h).Add(pt)
477 b := srcBounds.Intersect(r)
478 return b.Sub(b.Min)
479}
480
481func (p *cropToSizeFilter) Draw(dst draw.Image, src image.Image, options *Options) {
482 if p.w <= 0 || p.h <= 0 {
483 return
484 }
485 pt := anchorPt(src.Bounds(), p.w, p.h, p.anchor)
486 r := image.Rect(0, 0, p.w, p.h).Add(pt)
487 b := src.Bounds().Intersect(r)
488 Crop(b).Draw(dst, src, options)
489}
490
491// CropToSize creates a filter that crops an image to the specified size using the specified anchor point.
492func CropToSize(width, height int, anchor Anchor) Filter {
493 return &cropToSizeFilter{
494 w: width,
495 h: height,
496 anchor: anchor,
497 }
498}