1package gift
2
3import (
4 "image"
5 "image/draw"
6 "math"
7)
8
9func prepareLut(lutSize int, fn func(float32) float32) []float32 {
10 lut := make([]float32, lutSize)
11 q := 1 / float32(lutSize-1)
12 for v := 0; v < lutSize; v++ {
13 u := float32(v) * q
14 lut[v] = fn(u)
15 }
16 return lut
17}
18
19func getFromLut(lut []float32, u float32) float32 {
20 v := int(u*float32(len(lut)-1) + 0.5)
21 return lut[v]
22}
23
24type colorchanFilter struct {
25 fn func(float32) float32
26 lut bool
27}
28
29func (p *colorchanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
30 dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
31 return
32}
33
34func (p *colorchanFilter) 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 pixGetter := newPixelGetter(src)
42 pixSetter := newPixelSetter(dst)
43
44 var useLut bool
45 var lut []float32
46
47 useLut = false
48 if p.lut {
49 var lutSize int
50
51 it := pixGetter.it
52 if it == itNRGBA || it == itRGBA || it == itGray || it == itYCbCr {
53 lutSize = 0xff + 1
54 } else {
55 lutSize = 0xffff + 1
56 }
57
58 numCalculations := srcb.Dx() * srcb.Dy() * 3
59 if numCalculations > lutSize*2 {
60 useLut = true
61 lut = prepareLut(lutSize, p.fn)
62 }
63 }
64
65 parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
66 for y := pmin; y < pmax; y++ {
67 for x := srcb.Min.X; x < srcb.Max.X; x++ {
68 px := pixGetter.getPixel(x, y)
69 if useLut {
70 px.r = getFromLut(lut, px.r)
71 px.g = getFromLut(lut, px.g)
72 px.b = getFromLut(lut, px.b)
73 } else {
74 px.r = p.fn(px.r)
75 px.g = p.fn(px.g)
76 px.b = p.fn(px.b)
77 }
78 pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, px)
79 }
80 }
81 })
82}
83
84// Invert creates a filter that negates the colors of an image.
85func Invert() Filter {
86 return &colorchanFilter{
87 fn: func(x float32) float32 {
88 return 1 - x
89 },
90 lut: false,
91 }
92}
93
94// ColorspaceSRGBToLinear creates a filter that converts the colors of an image from sRGB to linear RGB.
95func ColorspaceSRGBToLinear() Filter {
96 return &colorchanFilter{
97 fn: func(x float32) float32 {
98 if x <= 0.04045 {
99 return x / 12.92
100 }
101 return float32(math.Pow(float64((x+0.055)/1.055), 2.4))
102 },
103 lut: true,
104 }
105}
106
107// ColorspaceLinearToSRGB creates a filter that converts the colors of an image from linear RGB to sRGB.
108func ColorspaceLinearToSRGB() Filter {
109 return &colorchanFilter{
110 fn: func(x float32) float32 {
111 if x <= 0.0031308 {
112 return x * 12.92
113 }
114 return float32(1.055*math.Pow(float64(x), 1/2.4) - 0.055)
115 },
116 lut: true,
117 }
118}
119
120// Gamma creates a filter that performs a gamma correction on an image.
121// The gamma parameter must be positive. Gamma = 1 gives the original image.
122// Gamma less than 1 darkens the image and gamma greater than 1 lightens it.
123func Gamma(gamma float32) Filter {
124 e := 1 / maxf32(gamma, 1.0e-5)
125 return &colorchanFilter{
126 fn: func(x float32) float32 {
127 return powf32(x, e)
128 },
129 lut: true,
130 }
131}
132
133func sigmoid(a, b, x float32) float32 {
134 return 1 / (1 + expf32(b*(a-x)))
135}
136
137// Sigmoid creates a filter that changes the contrast of an image using a sigmoidal function and returns the adjusted image.
138// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
139// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
140// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
141// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
142//
143// Example:
144//
145// g := gift.New(
146// gift.Sigmoid(0.5, 5),
147// )
148// dst := image.NewRGBA(g.Bounds(src.Bounds()))
149// g.Draw(dst, src)
150//
151func Sigmoid(midpoint, factor float32) Filter {
152 a := minf32(maxf32(midpoint, 0), 1)
153 b := absf32(factor)
154 sig0 := sigmoid(a, b, 0)
155 sig1 := sigmoid(a, b, 1)
156 e := float32(1.0e-5)
157
158 return &colorchanFilter{
159 fn: func(x float32) float32 {
160 if factor == 0 {
161 return x
162 } else if factor > 0 {
163 sig := sigmoid(a, b, x)
164 return (sig - sig0) / (sig1 - sig0)
165 } else {
166 arg := minf32(maxf32((sig1-sig0)*x+sig0, e), 1-e)
167 return a - logf32(1/arg-1)/b
168 }
169 },
170 lut: true,
171 }
172}
173
174// Contrast creates a filter that changes the contrast of an image.
175// The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image.
176// The percentage = -100 gives solid grey image. The percentage = 100 gives an overcontrasted image.
177func Contrast(percentage float32) Filter {
178 if percentage == 0 {
179 return ©imageFilter{}
180 }
181
182 p := 1 + minf32(maxf32(percentage, -100), 100)/100
183
184 return &colorchanFilter{
185 fn: func(x float32) float32 {
186 if 0 <= p && p <= 1 {
187 return 0.5 + (x-0.5)*p
188 } else if 1 < p && p < 2 {
189 return 0.5 + (x-0.5)*(1/(2.0-p))
190 } else {
191 if x < 0.5 {
192 return 0
193 }
194 return 1
195 }
196 },
197 lut: false,
198 }
199}
200
201// Brightness creates a filter that changes the brightness of an image.
202// The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image.
203// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
204func Brightness(percentage float32) Filter {
205 if percentage == 0 {
206 return ©imageFilter{}
207 }
208
209 shift := minf32(maxf32(percentage, -100), 100) / 100
210
211 return &colorchanFilter{
212 fn: func(x float32) float32 {
213 return x + shift
214 },
215 lut: false,
216 }
217}
218
219type colorFilter struct {
220 fn func(pixel) pixel
221}
222
223func (p *colorFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
224 dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
225 return
226}
227
228func (p *colorFilter) Draw(dst draw.Image, src image.Image, options *Options) {
229 if options == nil {
230 options = &defaultOptions
231 }
232
233 srcb := src.Bounds()
234 dstb := dst.Bounds()
235 pixGetter := newPixelGetter(src)
236 pixSetter := newPixelSetter(dst)
237
238 parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
239 for y := pmin; y < pmax; y++ {
240 for x := srcb.Min.X; x < srcb.Max.X; x++ {
241 px := pixGetter.getPixel(x, y)
242 pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, p.fn(px))
243 }
244 }
245 })
246}
247
248// Grayscale creates a filter that produces a grayscale version of an image.
249func Grayscale() Filter {
250 return &colorFilter{
251 fn: func(px pixel) pixel {
252 y := 0.299*px.r + 0.587*px.g + 0.114*px.b
253 return pixel{y, y, y, px.a}
254 },
255 }
256}
257
258// Sepia creates a filter that produces a sepia-toned version of an image.
259// The percentage parameter specifies how much the image should be adjusted. It must be in the range (0, 100)
260//
261// Example:
262//
263// g := gift.New(
264// gift.Sepia(100),
265// )
266// dst := image.NewRGBA(g.Bounds(src.Bounds()))
267// g.Draw(dst, src)
268//
269func Sepia(percentage float32) Filter {
270 adjustAmount := minf32(maxf32(percentage, 0), 100) / 100
271 rr := 1 - 0.607*adjustAmount
272 rg := 0.769 * adjustAmount
273 rb := 0.189 * adjustAmount
274 gr := 0.349 * adjustAmount
275 gg := 1 - 0.314*adjustAmount
276 gb := 0.168 * adjustAmount
277 br := 0.272 * adjustAmount
278 bg := 0.534 * adjustAmount
279 bb := 1 - 0.869*adjustAmount
280 return &colorFilter{
281 fn: func(px pixel) pixel {
282 r := px.r*rr + px.g*rg + px.b*rb
283 g := px.r*gr + px.g*gg + px.b*gb
284 b := px.r*br + px.g*bg + px.b*bb
285 return pixel{r, g, b, px.a}
286 },
287 }
288}
289
290func convertHSLToRGB(h, s, l float32) (float32, float32, float32) {
291 if s == 0 {
292 return l, l, l
293 }
294
295 _v := func(p, q, t float32) float32 {
296 if t < 0 {
297 t++
298 }
299 if t > 1 {
300 t--
301 }
302 if t < 1/6.0 {
303 return p + (q-p)*6*t
304 }
305 if t < 1/2.0 {
306 return q
307 }
308 if t < 2/3.0 {
309 return p + (q-p)*(2/3.0-t)*6
310 }
311 return p
312 }
313
314 var p, q float32
315 if l < 0.5 {
316 q = l * (1 + s)
317 } else {
318 q = l + s - l*s
319 }
320 p = 2*l - q
321
322 r := _v(p, q, h+1/3.0)
323 g := _v(p, q, h)
324 b := _v(p, q, h-1/3.0)
325
326 return r, g, b
327}
328
329func convertRGBToHSL(r, g, b float32) (float32, float32, float32) {
330 max := maxf32(r, maxf32(g, b))
331 min := minf32(r, minf32(g, b))
332
333 l := (max + min) / 2
334
335 if max == min {
336 return 0, 0, l
337 }
338
339 var h, s float32
340 d := max - min
341 if l > 0.5 {
342 s = d / (2 - max - min)
343 } else {
344 s = d / (max + min)
345 }
346
347 if r == max {
348 h = (g - b) / d
349 if g < b {
350 h += 6
351 }
352 } else if g == max {
353 h = (b-r)/d + 2
354 } else {
355 h = (r-g)/d + 4
356 }
357 h /= 6
358
359 return h, s, l
360}
361
362func normalizeHue(hue float32) float32 {
363 hue = hue - float32(int(hue))
364 if hue < 0 {
365 hue++
366 }
367 return hue
368}
369
370// Hue creates a filter that rotates the hue of an image.
371// The shift parameter is the hue angle shift, typically in range (-180, 180).
372// The shift = 0 gives the original image.
373func Hue(shift float32) Filter {
374 p := normalizeHue(shift / 360)
375 if p == 0 {
376 return ©imageFilter{}
377 }
378
379 return &colorFilter{
380 fn: func(px pixel) pixel {
381 h, s, l := convertRGBToHSL(px.r, px.g, px.b)
382 h = normalizeHue(h + p)
383 r, g, b := convertHSLToRGB(h, s, l)
384 return pixel{r, g, b, px.a}
385 },
386 }
387}
388
389// Saturation creates a filter that changes the saturation of an image.
390// The percentage parameter must be in range (-100, 500). The percentage = 0 gives the original image.
391func Saturation(percentage float32) Filter {
392 p := 1 + minf32(maxf32(percentage, -100), 500)/100
393 if p == 1 {
394 return ©imageFilter{}
395 }
396
397 return &colorFilter{
398 fn: func(px pixel) pixel {
399 h, s, l := convertRGBToHSL(px.r, px.g, px.b)
400 s *= p
401 if s > 1 {
402 s = 1
403 }
404 r, g, b := convertHSLToRGB(h, s, l)
405 return pixel{r, g, b, px.a}
406 },
407 }
408}
409
410// Colorize creates a filter that produces a colorized version of an image.
411// The hue parameter is the angle on the color wheel, typically in range (0, 360).
412// The saturation parameter must be in range (0, 100).
413// The percentage parameter specifies the strength of the effect, it must be in range (0, 100).
414//
415// Example:
416//
417// g := gift.New(
418// gift.Colorize(240, 50, 100), // blue colorization, 50% saturation
419// )
420// dst := image.NewRGBA(g.Bounds(src.Bounds()))
421// g.Draw(dst, src)
422//
423func Colorize(hue, saturation, percentage float32) Filter {
424 h := normalizeHue(hue / 360)
425 s := minf32(maxf32(saturation, 0), 100) / 100
426 p := minf32(maxf32(percentage, 0), 100) / 100
427 if p == 0 {
428 return ©imageFilter{}
429 }
430
431 return &colorFilter{
432 fn: func(px pixel) pixel {
433 _, _, l := convertRGBToHSL(px.r, px.g, px.b)
434 r, g, b := convertHSLToRGB(h, s, l)
435 px.r += (r - px.r) * p
436 px.g += (g - px.g) * p
437 px.b += (b - px.b) * p
438 return px
439 },
440 }
441}
442
443// ColorBalance creates a filter that changes the color balance of an image.
444// The percentage parameters for each color channel (red, green, blue) must be in range (-100, 500).
445//
446// Example:
447//
448// g := gift.New(
449// gift.ColorBalance(20, -20, 0), // +20% red, -20% green
450// )
451// dst := image.NewRGBA(g.Bounds(src.Bounds()))
452// g.Draw(dst, src)
453//
454func ColorBalance(percentageRed, percentageGreen, percentageBlue float32) Filter {
455 pr := 1 + minf32(maxf32(percentageRed, -100), 500)/100
456 pg := 1 + minf32(maxf32(percentageGreen, -100), 500)/100
457 pb := 1 + minf32(maxf32(percentageBlue, -100), 500)/100
458
459 return &colorFilter{
460 fn: func(px pixel) pixel {
461 px.r *= pr
462 px.g *= pg
463 px.b *= pb
464 return px
465 },
466 }
467}
468
469// Threshold creates a filter that applies black/white thresholding to the image.
470// The percentage parameter must be in range (0, 100).
471func Threshold(percentage float32) Filter {
472 p := minf32(maxf32(percentage, 0), 100) / 100
473 return &colorFilter{
474 fn: func(px pixel) pixel {
475 y := 0.299*px.r + 0.587*px.g + 0.114*px.b
476 if y > p {
477 return pixel{1, 1, 1, px.a}
478 }
479 return pixel{0, 0, 0, px.a}
480 },
481 }
482}
483
484// ColorFunc creates a filter that changes the colors of an image using custom function.
485// The fn parameter specifies a function that takes red, green, blue and alpha channels of a pixel
486// as float32 values in range (0, 1) and returns the modified channel values.
487//
488// Example:
489//
490// g := gift.New(
491// gift.ColorFunc(
492// func(r0, g0, b0, a0 float32) (r, g, b, a float32) {
493// r = 1 - r0 // invert the red channel
494// g = g0 + 0.1 // shift the green channel by 0.1
495// b = 0 // set the blue channel to 0
496// a = a0 // preserve the alpha channel
497// return
498// },
499// ),
500// )
501// dst := image.NewRGBA(g.Bounds(src.Bounds()))
502// g.Draw(dst, src)
503//
504func ColorFunc(fn func(r0, g0, b0, a0 float32) (r, g, b, a float32)) Filter {
505 return &colorFilter{
506 fn: func(px pixel) pixel {
507 r, g, b, a := fn(px.r, px.g, px.b, px.a)
508 return pixel{r, g, b, a}
509 },
510 }
511}