go.image/webp: implement lossy-with-alpha.

This fixes all three lossy_alpha*.webp conformance tests.

The test data was generated by cwebp/dwebp version 0.4.1:
cwebp yellow_rose.png -o yellow_rose.lossy-with-alpha.webp
dwebp yellow_rose.lossy-with-alpha.webp -pgm -o tmp.pgm
convert tmp.pgm yellow_rose.lossy-with-alpha.webp.nycbcra.png

LGTM=pascal.massimino, r
R=r, pascal.massimino
CC=golang-codereviews
https://golang.org/cl/154350043
diff --git a/cmd/webp-manual-test/main.go b/cmd/webp-manual-test/main.go
index f08c183..3d17734 100644
--- a/cmd/webp-manual-test/main.go
+++ b/cmd/webp-manual-test/main.go
@@ -17,10 +17,11 @@
 	"strings"
 
 	"code.google.com/p/go.image/webp"
+	"code.google.com/p/go.image/webp/nycbcra"
 )
 
 var (
-	dwebp = flag.String("dwebp", "", "path to the dwebp program "+
+	dwebp = flag.String("dwebp", "/usr/bin/dwebp", "path to the dwebp program "+
 		"installed from https://developers.google.com/speed/webp/download")
 	testdata = flag.String("testdata", "", "path to the libwebp-test-data directory "+
 		"checked out from https://chromium.googlesource.com/webm/libwebp-test-data")
@@ -32,6 +33,10 @@
 		flag.Usage()
 		log.Fatal("dwebp flag was not specified")
 	}
+	if _, err := os.Stat(*dwebp); err != nil {
+		flag.Usage()
+		log.Fatalf("could not find dwebp program at %q", *dwebp)
+	}
 	if *testdata == "" {
 		flag.Usage()
 		log.Fatal("testdata flag was not specified")
@@ -80,9 +85,9 @@
 	if err != nil {
 		return fmt.Errorf("Decode: %v", err)
 	}
-	format, encode := "-pam", encodePAM
-	if _, lossy := gotImage.(*image.YCbCr); lossy {
-		format, encode = "-pgm", encodePGM
+	format, encode := "-pgm", encodePGM
+	if _, lossless := gotImage.(*image.NRGBA); lossless {
+		format, encode = "-pam", encodePAM
 	}
 	got, err := encode(gotImage)
 	if err != nil {
@@ -130,8 +135,17 @@
 
 // encodePGM encodes gotImage in the PGM format in the IMC4 layout.
 func encodePGM(gotImage image.Image) ([]byte, error) {
-	m, ok := gotImage.(*image.YCbCr)
-	if !ok {
+	var (
+		m  *image.YCbCr
+		ma *nycbcra.Image
+	)
+	switch g := gotImage.(type) {
+	case *image.YCbCr:
+		m = g
+	case *nycbcra.Image:
+		m = &g.YCbCr
+		ma = g
+	default:
 		return nil, fmt.Errorf("lossy image did not decode to an *image.YCbCr")
 	}
 	if m.SubsampleRatio != image.YCbCrSubsampleRatio420 {
@@ -140,8 +154,12 @@
 	b := m.Bounds()
 	w, h := b.Dx(), b.Dy()
 	w2, h2 := (w+1)/2, (h+1)/2
+	outW, outH := 2*w2, h+h2
+	if ma != nil {
+		outH += h
+	}
 	buf := new(bytes.Buffer)
-	fmt.Fprintf(buf, "P5\n%d %d\n255\n", 2*w2, h+h2)
+	fmt.Fprintf(buf, "P5\n%d %d\n255\n", outW, outH)
 	for y := b.Min.Y; y < b.Max.Y; y++ {
 		o := m.YOffset(b.Min.X, y)
 		buf.Write(m.Y[o : o+w])
@@ -154,6 +172,15 @@
 		buf.Write(m.Cb[o : o+w2])
 		buf.Write(m.Cr[o : o+w2])
 	}
+	if ma != nil {
+		for y := b.Min.Y; y < b.Max.Y; y++ {
+			o := ma.AOffset(b.Min.X, y)
+			buf.Write(ma.A[o : o+w])
+			if w&1 != 0 {
+				buf.WriteByte(0x00)
+			}
+		}
+	}
 	return buf.Bytes(), nil
 }
 
diff --git a/testdata/yellow_rose.lossy-with-alpha.webp b/testdata/yellow_rose.lossy-with-alpha.webp
new file mode 100644
index 0000000..64d3b5d
--- /dev/null
+++ b/testdata/yellow_rose.lossy-with-alpha.webp
Binary files differ
diff --git a/testdata/yellow_rose.lossy-with-alpha.webp.nycbcra.png b/testdata/yellow_rose.lossy-with-alpha.webp.nycbcra.png
new file mode 100644
index 0000000..4445315
--- /dev/null
+++ b/testdata/yellow_rose.lossy-with-alpha.webp.nycbcra.png
Binary files differ
diff --git a/webp/decode.go b/webp/decode.go
index 453ccbd..8cf874b 100644
--- a/webp/decode.go
+++ b/webp/decode.go
@@ -9,6 +9,7 @@
 package webp
 
 import (
+	"bytes"
 	"errors"
 	"image"
 	"image/color"
@@ -16,11 +17,20 @@
 
 	"code.google.com/p/go.image/vp8"
 	"code.google.com/p/go.image/vp8l"
+	"code.google.com/p/go.image/webp/nycbcra"
 )
 
+// roundUp2 rounds u up to an even number.
+// https://developers.google.com/speed/webp/docs/riff_container#riff_file_format
+// says that "If Chunk Size is odd, a single padding byte... is added."
+func roundUp2(u uint32) uint32 {
+	return u + u&1
+}
+
 const (
 	formatVP8  = 1
 	formatVP8L = 2
+	formatVP8X = 3
 )
 
 func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) {
@@ -34,44 +44,153 @@
 		format = formatVP8
 	case "WEBPVP8L":
 		format = formatVP8L
+	case "WEBPVP8X":
+		format = formatVP8X
 	}
 	if string(b[:4]) != "RIFF" || format == 0 {
 		return nil, image.Config{}, errors.New("webp: invalid format")
 	}
 	riffLen := uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24
-	dataLen := uint32(b[16]) | uint32(b[17])<<8 | uint32(b[18])<<16 | uint32(b[19])<<24
+	dataLen := roundUp2(uint32(b[16]) | uint32(b[17])<<8 | uint32(b[18])<<16 | uint32(b[19])<<24)
 	if riffLen < dataLen+12 {
 		return nil, image.Config{}, errors.New("webp: invalid format")
 	}
-	if dataLen >= 1<<31 {
+	if dataLen == 0 || dataLen >= 1<<31 {
 		return nil, image.Config{}, errors.New("webp: invalid format")
 	}
 
-	if format == formatVP8 {
-		d := vp8.NewDecoder()
-		d.Init(r, int(dataLen))
-		fh, err := d.DecodeFrameHeader()
+	if format == formatVP8L {
+		r = &io.LimitedReader{R: r, N: int64(dataLen)}
+		if configOnly {
+			c, err := vp8l.DecodeConfig(r)
+			return nil, c, err
+		}
+		m, err := vp8l.Decode(r)
+		return m, image.Config{}, err
+	}
+
+	var (
+		alpha       []byte
+		alphaStride int
+	)
+	if format == formatVP8X {
+		if dataLen != 10 {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
+		if _, err := io.ReadFull(r, b[:10]); err != nil {
+			return nil, image.Config{}, err
+		}
+		const (
+			animationBit    = 1 << 1
+			xmpMetadataBit  = 1 << 2
+			exifMetadataBit = 1 << 3
+			alphaBit        = 1 << 4
+			iccProfileBit   = 1 << 5
+		)
+		if b[0] != alphaBit {
+			return nil, image.Config{}, errors.New("webp: non-Alpha VP8X is not implemented")
+		}
+		widthMinusOne := uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16
+		heightMinusOne := uint32(b[7]) | uint32(b[8])<<8 | uint32(b[9])<<16
+		if configOnly {
+			return nil, image.Config{
+				ColorModel: nycbcra.ColorModel,
+				Width:      int(widthMinusOne) + 1,
+				Height:     int(heightMinusOne) + 1,
+			}, nil
+		}
+
+		// Read the 8-byte chunk header plus the mandatory PFC (Pre-processing,
+		// Filter, Compression) byte.
+		if _, err := io.ReadFull(r, b[:9]); err != nil {
+			return nil, image.Config{}, err
+		}
+		if b[0] != 'A' || b[1] != 'L' || b[2] != 'P' || b[3] != 'H' {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
+		chunkLen := roundUp2(uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24)
+		// Subtract one byte from chunkLen, since we've already read the PFC byte.
+		if chunkLen == 0 {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
+		chunkLen--
+		filter := (b[8] >> 2) & 0x03
+		if filter != 0 {
+			return nil, image.Config{}, errors.New("webp: VP8X Alpha filtering != 0 is not implemented")
+		}
+		compression := b[8] & 0x03
+		if compression != 1 {
+			return nil, image.Config{}, errors.New("webp: VP8X Alpha compression != 1 is not implemented")
+		}
+
+		// Read the VP8L-compressed alpha values. First, synthesize a 5-byte VP8L header:
+		// a 1-byte magic number, a 14-bit widthMinusOne, a 14-bit heightMinusOne,
+		// a 1-bit (ignored, zero) alphaIsUsed and a 3-bit (zero) version.
+		// TODO(nigeltao): be more efficient than decoding an *image.NRGBA just to
+		// extract the green values to a separately allocated []byte. Fixing this
+		// will require changes to the vp8l package's API.
+		if widthMinusOne > 0x3fff || heightMinusOne > 0x3fff {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
+		b[0] = 0x2f // VP8L magic number.
+		b[1] = uint8(widthMinusOne)
+		b[2] = uint8(widthMinusOne>>8) | uint8(heightMinusOne<<6)
+		b[3] = uint8(heightMinusOne >> 2)
+		b[4] = uint8(heightMinusOne >> 10)
+		alphaImage, err := vp8l.Decode(io.MultiReader(
+			bytes.NewReader(b[:5]),
+			&io.LimitedReader{R: r, N: int64(chunkLen)},
+		))
 		if err != nil {
 			return nil, image.Config{}, err
 		}
-		if configOnly {
-			return nil, image.Config{
-				ColorModel: color.YCbCrModel,
-				Width:      fh.Width,
-				Height:     fh.Height,
-			}, nil
+		// The green values of the inner NRGBA image are the alpha values of the outer NYCbCrA image.
+		pix := alphaImage.(*image.NRGBA).Pix
+		alpha = make([]byte, len(pix)/4)
+		for i := range alpha {
+			alpha[i] = pix[4*i+1]
 		}
-		m, err := d.DecodeFrame()
-		return m, image.Config{}, nil
+		alphaStride = int(widthMinusOne) + 1
+
+		// The rest of the image should be in the lossy format. Check the "VP8 "
+		// header and fall through.
+		if _, err := io.ReadFull(r, b[:8]); err != nil {
+			return nil, image.Config{}, err
+		}
+		if b[0] != 'V' || b[1] != 'P' || b[2] != '8' || b[3] != ' ' {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
+		dataLen = roundUp2(uint32(b[4]) | uint32(b[5])<<8 | uint32(b[6])<<16 | uint32(b[7])<<24)
+		if dataLen == 0 || dataLen >= 1<<31 {
+			return nil, image.Config{}, errors.New("webp: invalid format")
+		}
 	}
 
-	r = &io.LimitedReader{R: r, N: int64(dataLen)}
-	if configOnly {
-		c, err := vp8l.DecodeConfig(r)
-		return nil, c, err
+	d := vp8.NewDecoder()
+	d.Init(r, int(dataLen))
+	fh, err := d.DecodeFrameHeader()
+	if err != nil {
+		return nil, image.Config{}, err
 	}
-	m, err := vp8l.Decode(r)
-	return m, image.Config{}, err
+	if configOnly {
+		return nil, image.Config{
+			ColorModel: color.YCbCrModel,
+			Width:      fh.Width,
+			Height:     fh.Height,
+		}, nil
+	}
+	m, err := d.DecodeFrame()
+	if err != nil {
+		return nil, image.Config{}, err
+	}
+	if alpha != nil {
+		return &nycbcra.Image{
+			YCbCr:   *m,
+			A:       alpha,
+			AStride: alphaStride,
+		}, image.Config{}, nil
+	}
+	return m, image.Config{}, nil
 }
 
 // Decode reads a WEBP image from r and returns it as an image.Image.
diff --git a/webp/decode_test.go b/webp/decode_test.go
index a86e32c..b1af0e7 100644
--- a/webp/decode_test.go
+++ b/webp/decode_test.go
@@ -13,6 +13,8 @@
 	"os"
 	"strings"
 	"testing"
+
+	"code.google.com/p/go.image/webp/nycbcra"
 )
 
 // hex is like fmt.Sprintf("% x", x) but also inserts dots every 16 bytes, to
@@ -30,6 +32,120 @@
 	return buf.String()
 }
 
+func testDecodeLossy(t *testing.T, tc string, withAlpha bool) {
+	webpFilename := "../testdata/" + tc + ".lossy.webp"
+	pngFilename := webpFilename + ".ycbcr.png"
+	if withAlpha {
+		webpFilename = "../testdata/" + tc + ".lossy-with-alpha.webp"
+		pngFilename = webpFilename + ".nycbcra.png"
+	}
+
+	f0, err := os.Open(webpFilename)
+	if err != nil {
+		t.Errorf("%s: Open WEBP: %v", tc, err)
+		return
+	}
+	defer f0.Close()
+	img0, err := Decode(f0)
+	if err != nil {
+		t.Errorf("%s: Decode WEBP: %v", tc, err)
+		return
+	}
+
+	var (
+		m0 *image.YCbCr
+		a0 *nycbcra.Image
+		ok bool
+	)
+	if withAlpha {
+		a0, ok = img0.(*nycbcra.Image)
+		if ok {
+			m0 = &a0.YCbCr
+		}
+	} else {
+		m0, ok = img0.(*image.YCbCr)
+	}
+	if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 {
+		t.Errorf("%s: decoded WEBP image is not a 4:2:0 YCbCr or 4:2:0 NYCbCrA", tc)
+		return
+	}
+	// w2 and h2 are the half-width and half-height, rounded up.
+	w, h := m0.Bounds().Dx(), m0.Bounds().Dy()
+	w2, h2 := int((w+1)/2), int((h+1)/2)
+
+	f1, err := os.Open(pngFilename)
+	if err != nil {
+		t.Errorf("%s: Open PNG: %v", tc, err)
+		return
+	}
+	defer f1.Close()
+	img1, err := png.Decode(f1)
+	if err != nil {
+		t.Errorf("%s: Open PNG: %v", tc, err)
+		return
+	}
+
+	// The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high
+	// (or 2*h+h2 high, if with Alpha) gray image arranged in IMC4 format:
+	//   YYYY
+	//   YYYY
+	//   BBRR
+	//   AAAA
+	// See http://www.fourcc.org/yuv.php#IMC4
+	pngW, pngH := 2*w2, h+h2
+	if withAlpha {
+		pngH += h
+	}
+	if got, want := img1.Bounds(), image.Rect(0, 0, pngW, pngH); got != want {
+		t.Errorf("%s: bounds0: got %v, want %v", tc, got, want)
+		return
+	}
+	m1, ok := img1.(*image.Gray)
+	if !ok {
+		t.Errorf("%s: decoded PNG image is not a Gray", tc)
+		return
+	}
+
+	type plane struct {
+		name     string
+		m0Pix    []uint8
+		m0Stride int
+		m1Rect   image.Rectangle
+	}
+	planes := []plane{
+		{"Y", m0.Y, m0.YStride, image.Rect(0, 0, w, h)},
+		{"Cb", m0.Cb, m0.CStride, image.Rect(0*w2, h, 1*w2, h+h2)},
+		{"Cr", m0.Cr, m0.CStride, image.Rect(1*w2, h, 2*w2, h+h2)},
+	}
+	if withAlpha {
+		planes = append(planes, plane{
+			"A", a0.A, a0.AStride, image.Rect(0, h+h2, w, 2*h+h2),
+		})
+	}
+
+	for _, plane := range planes {
+		dx := plane.m1Rect.Dx()
+		nDiff, diff := 0, make([]byte, dx)
+		for j, y := 0, plane.m1Rect.Min.Y; y < plane.m1Rect.Max.Y; j, y = j+1, y+1 {
+			got := plane.m0Pix[j*plane.m0Stride:][:dx]
+			want := m1.Pix[y*m1.Stride+plane.m1Rect.Min.X:][:dx]
+			if bytes.Equal(got, want) {
+				continue
+			}
+			nDiff++
+			if nDiff > 10 {
+				t.Errorf("%s: %s plane: more rows differ", tc, plane.name)
+				break
+			}
+			for i := range got {
+				diff[i] = got[i] - want[i]
+			}
+			t.Errorf("%s: %s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s",
+				tc, plane.name, j, y, hex(got), hex(want), hex(diff))
+		}
+	}
+}
+
 func TestDecodeVP8(t *testing.T) {
 	testCases := []string{
 		"blue-purple-pink",
@@ -41,86 +157,17 @@
 	}
 
 	for _, tc := range testCases {
-		f0, err := os.Open("../testdata/" + tc + ".lossy.webp")
-		if err != nil {
-			t.Errorf("%s: Open WEBP: %v", tc, err)
-			continue
-		}
-		defer f0.Close()
-		img0, err := Decode(f0)
-		if err != nil {
-			t.Errorf("%s: Decode WEBP: %v", tc, err)
-			continue
-		}
+		testDecodeLossy(t, tc, false)
+	}
+}
 
-		m0, ok := img0.(*image.YCbCr)
-		if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 {
-			t.Errorf("%s: decoded WEBP image is not a 4:2:0 YCbCr", tc)
-			continue
-		}
-		// w2 and h2 are the half-width and half-height, rounded up.
-		w, h := m0.Bounds().Dx(), m0.Bounds().Dy()
-		w2, h2 := int((w+1)/2), int((h+1)/2)
+func TestDecodeVP8XAlpha(t *testing.T) {
+	testCases := []string{
+		"yellow_rose",
+	}
 
-		f1, err := os.Open("../testdata/" + tc + ".lossy.webp.ycbcr.png")
-		if err != nil {
-			t.Errorf("%s: Open PNG: %v", tc, err)
-			continue
-		}
-		defer f1.Close()
-		img1, err := png.Decode(f1)
-		if err != nil {
-			t.Errorf("%s: Open PNG: %v", tc, err)
-			continue
-		}
-
-		// The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high
-		// gray image arranged in IMC4 format:
-		//   YYYY
-		//   YYYY
-		//   BBRR
-		// See http://www.fourcc.org/yuv.php#IMC4
-		if got, want := img1.Bounds(), image.Rect(0, 0, 2*w2, h+h2); got != want {
-			t.Errorf("%s: bounds0: got %v, want %v", tc, got, want)
-			continue
-		}
-		m1, ok := img1.(*image.Gray)
-		if !ok {
-			t.Errorf("%s: decoded PNG image is not a Gray", tc)
-			continue
-		}
-
-		planes := []struct {
-			name     string
-			m0Pix    []uint8
-			m0Stride int
-			m1Rect   image.Rectangle
-		}{
-			{"Y", m0.Y, m0.YStride, image.Rect(0, 0, w, h)},
-			{"Cb", m0.Cb, m0.CStride, image.Rect(0*w2, h, 1*w2, h+h2)},
-			{"Cr", m0.Cr, m0.CStride, image.Rect(1*w2, h, 2*w2, h+h2)},
-		}
-		for _, plane := range planes {
-			dx := plane.m1Rect.Dx()
-			nDiff, diff := 0, make([]byte, dx)
-			for j, y := 0, plane.m1Rect.Min.Y; y < plane.m1Rect.Max.Y; j, y = j+1, y+1 {
-				got := plane.m0Pix[j*plane.m0Stride:][:dx]
-				want := m1.Pix[y*m1.Stride+plane.m1Rect.Min.X:][:dx]
-				if bytes.Equal(got, want) {
-					continue
-				}
-				nDiff++
-				if nDiff > 10 {
-					t.Errorf("%s: %s plane: more rows differ", tc, plane.name)
-					break
-				}
-				for i := range got {
-					diff[i] = got[i] - want[i]
-				}
-				t.Errorf("%s: %s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s",
-					tc, plane.name, j, y, hex(got), hex(want), hex(diff))
-			}
-		}
+	for _, tc := range testCases {
+		testDecodeLossy(t, tc, true)
 	}
 }