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)
}
}