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 @@ import ( "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 @@ func main() { 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 @@ func test(name string) error { 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 @@ func encodePAM(gotImage image.Image) ([]byte, error) { // 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 @@ func encodePGM(gotImage image.Image) ([]byte, error) { 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 @@ func encodePGM(gotImage image.Image) ([]byte, error) { 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 Binary files /dev/null and b/testdata/yellow_rose.lossy-with-alpha.webp 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 Binary files /dev/null and b/testdata/yellow_rose.lossy-with-alpha.webp.nycbcra.png 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 @@ import ( "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 @@ func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { 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] + } + 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") } - m, err := d.DecodeFrame() - return m, image.Config{}, nil } - 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 @@ import ( "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 @@ func hex(x []byte) string { 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 @@ func TestDecodeVP8(t *testing.T) { } 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) } }