transform.go

  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}