draw: clamp kernel output so red, green and blue <= alpha.

The raw computation can produce red > alpha when some weights are
negative.

Change-Id: Ic6701354770f012d3ef21a390a8400e14e9d1e25
Reviewed-on: https://go-review.googlesource.com/8740
Reviewed-by: Rob Pike <r@golang.org>
diff --git a/draw/gen.go b/draw/gen.go
index 1e335e0..3295c2d 100644
--- a/draw/gen.go
+++ b/draw/gen.go
@@ -286,6 +286,24 @@
 			)
 		}
 
+	case "clampToAlpha":
+		if alwaysOpaque[d.sType] {
+			return ";"
+		}
+		// Go uses alpha-premultiplied color. The naive computation can lead to
+		// invalid colors, e.g. red > alpha, when some weights are negative.
+		return `
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+		`
+
 	case "outputu":
 		// TODO: handle op==Over, not just op==Src.
 		args, _ := splitArgs(suffix)
@@ -1118,6 +1136,7 @@
 						pb += p[2] * c.weight
 						pa += p[3] * c.weight
 					}
+					$clampToAlpha
 					$outputf[dr.Min.X + int(dx), dr.Min.Y + int(adr.Min.Y + dy), ftou, p, s.invTotalWeight]
 					$tweakD
 				}
@@ -1215,6 +1234,7 @@
 							}
 						}
 					}
+					$clampToAlpha
 					$outputf[dr.Min.X + int(dx), dr.Min.Y + int(dy), fffftou, p, 1]
 				}
 			}
diff --git a/draw/impl.go b/draw/impl.go
index 8fed0bd..4ad48ca 100644
--- a/draw/impl.go
+++ b/draw/impl.go
@@ -4494,6 +4494,17 @@
 				pb += p[2] * c.weight
 				pa += p[3] * c.weight
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(ftou(pr*s.invTotalWeight) >> 8)
 			dst.Pix[d+1] = uint8(ftou(pg*s.invTotalWeight) >> 8)
 			dst.Pix[d+2] = uint8(ftou(pb*s.invTotalWeight) >> 8)
@@ -4515,6 +4526,17 @@
 				pb += p[2] * c.weight
 				pa += p[3] * c.weight
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(ftou(pr*s.invTotalWeight) >> 8)
 			dst.Pix[d+1] = uint8(ftou(pg*s.invTotalWeight) >> 8)
 			dst.Pix[d+2] = uint8(ftou(pb*s.invTotalWeight) >> 8)
@@ -4537,6 +4559,17 @@
 				pb += p[2] * c.weight
 				pa += p[3] * c.weight
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dstColorRGBA64.R = ftou(pr * s.invTotalWeight)
 			dstColorRGBA64.G = ftou(pg * s.invTotalWeight)
 			dstColorRGBA64.B = ftou(pb * s.invTotalWeight)
@@ -4559,6 +4592,17 @@
 				pb += p[2] * c.weight
 				pa += p[3] * c.weight
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dstColorRGBA64.R = ftou(pr * s.invTotalWeight)
 			dstColorRGBA64.G = ftou(pg * s.invTotalWeight)
 			dstColorRGBA64.B = ftou(pb * s.invTotalWeight)
@@ -4763,6 +4807,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -4867,6 +4922,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -4971,6 +5037,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -5075,6 +5152,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -5671,6 +5759,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -5771,6 +5870,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dst.Pix[d+0] = uint8(fffftou(pr) >> 8)
 			dst.Pix[d+1] = uint8(fffftou(pg) >> 8)
 			dst.Pix[d+2] = uint8(fffftou(pb) >> 8)
@@ -5872,6 +5982,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dstColorRGBA64.R = fffftou(pr)
 			dstColorRGBA64.G = fffftou(pg)
 			dstColorRGBA64.B = fffftou(pb)
@@ -5974,6 +6095,17 @@
 					}
 				}
 			}
+
+			if pr > pa {
+				pr = pa
+			}
+			if pg > pa {
+				pg = pa
+			}
+			if pb > pa {
+				pb = pa
+			}
+
 			dstColorRGBA64.R = fffftou(pr)
 			dstColorRGBA64.G = fffftou(pg)
 			dstColorRGBA64.B = fffftou(pb)
diff --git a/draw/scale_test.go b/draw/scale_test.go
index 65a536a..47a06bf 100644
--- a/draw/scale_test.go
+++ b/draw/scale_test.go
@@ -115,6 +115,43 @@
 func TestScaleUp(t *testing.T)   { testInterp(t, 75, 100, "up", "14x18.png") }
 func TestTransform(t *testing.T) { testInterp(t, 100, 100, "rotate", "14x18.png") }
 
+// TestNegativeWeights tests that scaling by a kernel that produces negative
+// weights, such as the Catmull-Rom kernel, doesn't produce an invalid color
+// according to Go's alpha-premultiplied model.
+func TestNegativeWeights(t *testing.T) {
+	check := func(m *image.RGBA) error {
+		b := m.Bounds()
+		for y := b.Min.Y; y < b.Max.Y; y++ {
+			for x := b.Min.X; x < b.Max.X; x++ {
+				if c := m.RGBAAt(x, y); c.R > c.A || c.G > c.A || c.B > c.A {
+					return fmt.Errorf("invalid color.RGBA at (%d, %d): %v", x, y, c)
+				}
+			}
+		}
+		return nil
+	}
+
+	src := image.NewRGBA(image.Rect(0, 0, 16, 16))
+	for y := 0; y < 16; y++ {
+		for x := 0; x < 16; x++ {
+			a := y * 0x11
+			src.Set(x, y, color.RGBA{
+				R: uint8(x * 0x11 * a / 0xff),
+				A: uint8(a),
+			})
+		}
+	}
+	if err := check(src); err != nil {
+		t.Fatalf("src image: %v", err)
+	}
+
+	dst := image.NewRGBA(image.Rect(0, 0, 32, 32))
+	CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), nil)
+	if err := check(dst); err != nil {
+		t.Fatalf("dst image: %v", err)
+	}
+}
+
 func fillPix(r *rand.Rand, pixs ...[]byte) {
 	for _, pix := range pixs {
 		for i := range pix {