Merge pull request #130 from StruffelProductions/add-hue-adjustment

Add function "AdjustHue"
diff --git a/README.md b/README.md
index eecdb3d..4963add 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,16 @@
 -----------------------------------|----------------------------------------------|---------------------------------------------

 ![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_saturation_p30.png) | ![dstImage](testdata/out_saturation_m30.png)

 

+### Hue adjustment

+

+```go

+dstImage := imaging.AdjustHue(srcImage, 20)

+```

+

+Original image                     | Hue = 60                                     | Hue = -60

+-----------------------------------|----------------------------------------------|---------------------------------------------

+![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_hue_p60.png) | ![dstImage](testdata/out_hue_m60.png)

+

 ## FAQ

 

 ### Incorrect image orientation after processing (e.g. an image appears rotated after resizing)

diff --git a/adjust.go b/adjust.go
index 03ec97f..971aebc 100644
--- a/adjust.go
+++ b/adjust.go
@@ -80,6 +80,34 @@
 	})
 }
 
+// AdjustHue changes the hue of the image using the shift parameter (measured in degrees) and returns the adjusted image.
+// The shift = 0 (or 360 / -360 / etc.) gives the original image.
+// The shift = 180 (or -180) corresponds to a 180° degree rotation of the color wheel and thus gives the image with its hue inverted for each pixel.
+//
+// Examples:
+//  dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°.
+//  dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°.
+//
+func AdjustHue(img image.Image, shift float64) *image.NRGBA {
+	if math.Mod(shift, 360) == 0 {
+		return Clone(img)
+	}
+
+	summand := shift / 360
+
+	return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
+		h, s, l := rgbToHSL(c.R, c.G, c.B)
+		h += summand
+		h = math.Mod(h, 1)
+		//Adding 1 because Golang's Modulo function behaves differently to similar operators in most other languages.
+		if h < 0 {
+			h++
+		}
+		r, g, b := hslToRGB(h, s, l)
+		return color.NRGBA{r, g, b, c.A}
+	})
+}
+
 // AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
 // The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
 // The percentage = -100 gives solid gray image.
diff --git a/adjust_test.go b/adjust_test.go
index 49a7cc6..c928676 100644
--- a/adjust_test.go
+++ b/adjust_test.go
@@ -247,6 +247,293 @@
 	}
 }
 
+func TestAdjustHue(t *testing.T) {
+	testCases := []struct {
+		name string
+		src  image.Image
+		p    float64
+		want *image.NRGBA
+	}{
+		{
+			"AdjustHue 3x3 -540",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			-540,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+					0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 -360",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			-360,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 -350",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			-350,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
+					0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 -180",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			-180,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+					0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 -10",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			-10,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
+					0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 0",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			0,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 10",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			10,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
+					0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 180",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			180,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+					0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 350",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			350,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
+					0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 360",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			360,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustHue 3x3 540",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			540,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+					0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustHue(tc.src, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
+	}
+}
+
+func TestAdjustHueGolden(t *testing.T) {
+	for name, p := range map[string]float64{
+		"out_hue_m480.png": -480,
+		"out_hue_m120.png": -120,
+		"out_hue_m60.png":  -60,
+		"out_hue_p60.png":  60,
+		"out_hue_p120.png": 120,
+		"out_hue_p480.png": 480,
+	} {
+		got := AdjustHue(testdataFlowersSmallPNG, p)
+		want, err := Open("testdata/" + name)
+		if err != nil {
+			t.Fatalf("failed to open image: %v", err)
+		}
+		if !compareNRGBAGolden(got, toNRGBA(want)) {
+			t.Errorf("resulting image differs from golden: %s", name)
+		}
+	}
+}
+
+func BenchmarkAdjustHue(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		AdjustHue(testdataBranchesJPG, 10)
+	}
+}
+
 func TestAdjustContrast(t *testing.T) {
 	testCases := []struct {
 		name string
diff --git a/testdata/out_hue_m120.png b/testdata/out_hue_m120.png
new file mode 100644
index 0000000..32b2b37
--- /dev/null
+++ b/testdata/out_hue_m120.png
Binary files differ
diff --git a/testdata/out_hue_m480.png b/testdata/out_hue_m480.png
new file mode 100644
index 0000000..32b2b37
--- /dev/null
+++ b/testdata/out_hue_m480.png
Binary files differ
diff --git a/testdata/out_hue_m60.png b/testdata/out_hue_m60.png
new file mode 100644
index 0000000..da28824
--- /dev/null
+++ b/testdata/out_hue_m60.png
Binary files differ
diff --git a/testdata/out_hue_p120.png b/testdata/out_hue_p120.png
new file mode 100644
index 0000000..28d4c55
--- /dev/null
+++ b/testdata/out_hue_p120.png
Binary files differ
diff --git a/testdata/out_hue_p480.png b/testdata/out_hue_p480.png
new file mode 100644
index 0000000..28d4c55
--- /dev/null
+++ b/testdata/out_hue_p480.png
Binary files differ
diff --git a/testdata/out_hue_p60.png b/testdata/out_hue_p60.png
new file mode 100644
index 0000000..44c62e8
--- /dev/null
+++ b/testdata/out_hue_p60.png
Binary files differ