gift.go

  1/*
  2
  3Package gift provides a set of useful image processing filters.
  4
  5Basic usage:
  6
  7	// 1. Create a new GIFT and add some filters:
  8
  9	g := gift.New(
 10	    gift.Resize(800, 0, gift.LanczosResampling),
 11	    gift.UnsharpMask(1, 1, 0),
 12	)
 13
 14	// 2. Create a new image of the corresponding size.
 15	// dst is a new target image, src is the original image
 16
 17	dst := image.NewRGBA(g.Bounds(src.Bounds()))
 18
 19	// 3. Use Draw func to apply the filters to src and store the result in dst:
 20
 21	g.Draw(dst, src)
 22
 23*/
 24package gift
 25
 26import (
 27	"image"
 28	"image/draw"
 29)
 30
 31// Filter is an image processing filter.
 32type Filter interface {
 33	// Draw applies the filter to the src image and outputs the result to the dst image.
 34	Draw(dst draw.Image, src image.Image, options *Options)
 35	// Bounds calculates the appropriate bounds of an image after applying the filter.
 36	Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle)
 37}
 38
 39// Options is the parameters passed to image processing filters.
 40type Options struct {
 41	Parallelization bool
 42}
 43
 44var defaultOptions = Options{
 45	Parallelization: true,
 46}
 47
 48// GIFT implements a list of filters that can be applied to an image at once.
 49type GIFT struct {
 50	Filters []Filter
 51	Options Options
 52}
 53
 54// New creates a new instance of the filter toolkit and initializes it with the given list of filters.
 55func New(filters ...Filter) *GIFT {
 56	return &GIFT{
 57		Filters: filters,
 58		Options: defaultOptions,
 59	}
 60}
 61
 62// SetParallelization enables or disables faster image processing using parallel goroutines.
 63// Parallelization is enabled by default.
 64func (g *GIFT) SetParallelization(isEnabled bool) {
 65	g.Options.Parallelization = isEnabled
 66}
 67
 68// Parallelization returns the current state of parallelization option.
 69func (g *GIFT) Parallelization() bool {
 70	return g.Options.Parallelization
 71}
 72
 73// Add appends the given filters to the list of filters.
 74func (g *GIFT) Add(filters ...Filter) {
 75	g.Filters = append(g.Filters, filters...)
 76}
 77
 78// Empty removes all the filters from the list.
 79func (g *GIFT) Empty() {
 80	g.Filters = []Filter{}
 81}
 82
 83// Bounds calculates the appropriate bounds for the result image after applying all the added filters.
 84// Parameter srcBounds is the bounds of the source image.
 85//
 86// Example:
 87//
 88// 	src := image.NewRGBA(image.Rect(0, 0, 100, 200))
 89//	g := gift.New(gift.Rotate90())
 90//
 91// 	// calculate image bounds after applying rotation and create a new image of that size.
 92// 	dst := image.NewRGBA(g.Bounds(src.Bounds())) // dst bounds: (0, 0, 200, 100)
 93//
 94//
 95func (g *GIFT) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
 96	b := srcBounds
 97	for _, f := range g.Filters {
 98		b = f.Bounds(b)
 99	}
100	dstBounds = b
101	return
102}
103
104// Draw applies all the added filters to the src image and outputs the result to the dst image.
105func (g *GIFT) Draw(dst draw.Image, src image.Image) {
106	if len(g.Filters) == 0 {
107		copyimage(dst, src, &g.Options)
108		return
109	}
110
111	first, last := 0, len(g.Filters)-1
112	var tmpIn image.Image
113	var tmpOut draw.Image
114
115	for i, f := range g.Filters {
116		if i == first {
117			tmpIn = src
118		} else {
119			tmpIn = tmpOut
120		}
121
122		if i == last {
123			tmpOut = dst
124		} else {
125			tmpOut = createTempImage(f.Bounds(tmpIn.Bounds()))
126		}
127
128		f.Draw(tmpOut, tmpIn, &g.Options)
129	}
130}
131
132// Operator is an image composition operator.
133type Operator int
134
135// Composition operators.
136const (
137	CopyOperator Operator = iota
138	OverOperator
139)
140
141// DrawAt applies all the added filters to the src image and outputs the result to the dst image
142// at the specified position pt using the specified composition operator op.
143func (g *GIFT) DrawAt(dst draw.Image, src image.Image, pt image.Point, op Operator) {
144	switch op {
145	case OverOperator:
146		tb := g.Bounds(src.Bounds())
147		tb = tb.Sub(tb.Min).Add(pt)
148		tmp := createTempImage(tb)
149		g.Draw(tmp, src)
150		pixGetterDst := newPixelGetter(dst)
151		pixGetterTmp := newPixelGetter(tmp)
152		pixSetterDst := newPixelSetter(dst)
153		ib := tb.Intersect(dst.Bounds())
154		parallelize(g.Options.Parallelization, ib.Min.Y, ib.Max.Y, func(pmin, pmax int) {
155			for y := pmin; y < pmax; y++ {
156				for x := ib.Min.X; x < ib.Max.X; x++ {
157					px0 := pixGetterDst.getPixel(x, y)
158					px1 := pixGetterTmp.getPixel(x, y)
159					c1 := px1.a
160					c0 := (1 - c1) * px0.a
161					cs := c0 + c1
162					c0 /= cs
163					c1 /= cs
164					r := px0.r*c0 + px1.r*c1
165					g := px0.g*c0 + px1.g*c1
166					b := px0.b*c0 + px1.b*c1
167					a := px0.a + px1.a*(1-px0.a)
168					pixSetterDst.setPixel(x, y, pixel{r, g, b, a})
169				}
170			}
171		})
172
173	default:
174		if pt.Eq(dst.Bounds().Min) {
175			g.Draw(dst, src)
176			return
177		}
178		if subimg, ok := getSubImage(dst, pt); ok {
179			g.Draw(subimg, src)
180			return
181		}
182		tb := g.Bounds(src.Bounds())
183		tb = tb.Sub(tb.Min).Add(pt)
184		tmp := createTempImage(tb)
185		g.Draw(tmp, src)
186		pixGetter := newPixelGetter(tmp)
187		pixSetter := newPixelSetter(dst)
188		ib := tb.Intersect(dst.Bounds())
189		parallelize(g.Options.Parallelization, ib.Min.Y, ib.Max.Y, func(pmin, pmax int) {
190			for y := pmin; y < pmax; y++ {
191				for x := ib.Min.X; x < ib.Max.X; x++ {
192					pixSetter.setPixel(x, y, pixGetter.getPixel(x, y))
193				}
194			}
195		})
196	}
197}
198
199func getSubImage(img draw.Image, pt image.Point) (draw.Image, bool) {
200	if !pt.In(img.Bounds()) {
201		return nil, false
202	}
203	switch img := img.(type) {
204	case *image.Gray:
205		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
206	case *image.Gray16:
207		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
208	case *image.RGBA:
209		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
210	case *image.RGBA64:
211		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
212	case *image.NRGBA:
213		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
214	case *image.NRGBA64:
215		return img.SubImage(image.Rectangle{pt, img.Bounds().Max}).(draw.Image), true
216	default:
217		return nil, false
218	}
219}