From 697863cec6564958b27cfcef039bdb04324c2ec8 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Fri, 10 Apr 2015 13:05:20 +1000 Subject: [PATCH] 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 --- draw/gen.go | 20 +++++++ draw/impl.go | 132 +++++++++++++++++++++++++++++++++++++++++++++ draw/scale_test.go | 37 +++++++++++++ 3 files changed, 189 insertions(+) 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 @@ func expnDollar(prefix, dollar, suffix string, d *data) string { ) } + 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 @@ const ( 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 @@ const ( } } } + $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 @@ func (z *kernelScaler) scaleY_RGBA_Over(dst *image.RGBA, dr, adr image.Rectangle 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 @@ func (z *kernelScaler) scaleY_RGBA_Src(dst *image.RGBA, dr, adr image.Rectangle, 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 @@ func (z *kernelScaler) scaleY_Image_Over(dst Image, dr, adr image.Rectangle, tmp 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 @@ func (z *kernelScaler) scaleY_Image_Src(dst Image, dr, adr image.Rectangle, tmp 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 @@ func (q *Kernel) transform_RGBA_NRGBA_Over(dst *image.RGBA, dr, adr image.Rectan } } } + + 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 @@ func (q *Kernel) transform_RGBA_NRGBA_Src(dst *image.RGBA, dr, adr image.Rectang } } } + + 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 @@ func (q *Kernel) transform_RGBA_RGBA_Over(dst *image.RGBA, dr, adr image.Rectang } } } + + 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 @@ func (q *Kernel) transform_RGBA_RGBA_Src(dst *image.RGBA, dr, adr image.Rectangl } } } + + 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 @@ func (q *Kernel) transform_RGBA_Image_Over(dst *image.RGBA, dr, adr image.Rectan } } } + + 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 @@ func (q *Kernel) transform_RGBA_Image_Src(dst *image.RGBA, dr, adr image.Rectang } } } + + 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 @@ func (q *Kernel) transform_Image_Image_Over(dst Image, dr, adr image.Rectangle, } } } + + 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 @@ func (q *Kernel) transform_Image_Image_Src(dst Image, dr, adr image.Rectangle, d } } } + + 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 TestScaleDown(t *testing.T) { testInterp(t, 100, 100, "down", "280x360.jpeg 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 {