go.image/riff: new package.
Also update package webp to use package riff.
LGTM=r
R=r
CC=golang-codereviews, pascal.massimino
https://golang.org/cl/162850043
diff --git a/riff/example_test.go b/riff/example_test.go
new file mode 100644
index 0000000..faa4975
--- /dev/null
+++ b/riff/example_test.go
@@ -0,0 +1,113 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package riff_test
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "strings"
+
+ "code.google.com/p/go.image/riff"
+)
+
+func ExampleReader() {
+ formType, r, err := riff.NewReader(strings.NewReader(data))
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("RIFF(%s)\n", formType)
+ if err := dump(r, ".\t"); err != nil {
+ log.Fatal(err)
+ }
+ // Output:
+ // RIFF(ROOT)
+ // . ZERO ""
+ // . ONE "a"
+ // . LIST(META)
+ // . . LIST(GOOD)
+ // . . . ONE "a"
+ // . . . FIVE "klmno"
+ // . . ZERO ""
+ // . . LIST(BAD )
+ // . . . THRE "def"
+ // . TWO "bc"
+ // . LIST(UGLY)
+ // . . FOUR "ghij"
+ // . . SIX "pqrstu"
+}
+
+func dump(r *riff.Reader, indent string) error {
+ for {
+ chunkID, chunkLen, chunkData, err := r.Next()
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if chunkID == riff.LIST {
+ listType, list, err := riff.NewListReader(chunkLen, chunkData)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%sLIST(%s)\n", indent, listType)
+ if err := dump(list, indent+".\t"); err != nil {
+ return err
+ }
+ continue
+ }
+ b, err := ioutil.ReadAll(chunkData)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%s%s %q\n", indent, chunkID, b)
+ }
+}
+
+func encodeU32(u uint32) string {
+ return string([]byte{
+ byte(u >> 0),
+ byte(u >> 8),
+ byte(u >> 16),
+ byte(u >> 24),
+ })
+}
+
+func encode(chunkID, contents string) string {
+ n := len(contents)
+ if n&1 == 1 {
+ contents += "\x00"
+ }
+ return chunkID + encodeU32(uint32(n)) + contents
+}
+
+func encodeMulti(typ0, typ1 string, chunks ...string) string {
+ n := 4
+ for _, c := range chunks {
+ n += len(c)
+ }
+ s := typ0 + encodeU32(uint32(n)) + typ1
+ for _, c := range chunks {
+ s += c
+ }
+ return s
+}
+
+var (
+ d0 = encode("ZERO", "")
+ d1 = encode("ONE ", "a")
+ d2 = encode("TWO ", "bc")
+ d3 = encode("THRE", "def")
+ d4 = encode("FOUR", "ghij")
+ d5 = encode("FIVE", "klmno")
+ d6 = encode("SIX ", "pqrstu")
+ l0 = encodeMulti("LIST", "GOOD", d1, d5)
+ l1 = encodeMulti("LIST", "BAD ", d3)
+ l2 = encodeMulti("LIST", "UGLY", d4, d6)
+ l01 = encodeMulti("LIST", "META", l0, d0, l1)
+ data = encodeMulti("RIFF", "ROOT", d0, d1, l01, d2, l2)
+)
diff --git a/riff/riff.go b/riff/riff.go
new file mode 100644
index 0000000..2b50d8c
--- /dev/null
+++ b/riff/riff.go
@@ -0,0 +1,180 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package riff implements the Resource Interchange File Format, used by media
+// formats such as AVI, WAVE and WEBP.
+//
+// A RIFF stream contains a sequence of chunks. Each chunk consists of an 8-byte
+// header (containing a 4-byte chunk type and a 4-byte chunk length), the chunk
+// data (presented as an io.Reader), and some padding bytes.
+//
+// A detailed description of the format is at
+// http://www.tactilemedia.com/info/MCI_Control_Info.html
+package riff
+
+import (
+ "errors"
+ "io"
+ "io/ioutil"
+ "math"
+)
+
+var (
+ errMissingPaddingByte = errors.New("riff: missing padding byte")
+ errMissingRIFFChunkHeader = errors.New("riff: missing RIFF chunk header")
+ errShortChunkData = errors.New("riff: short chunk data")
+ errShortChunkHeader = errors.New("riff: short chunk header")
+ errStaleReader = errors.New("riff: stale reader")
+)
+
+// u32 decodes the first four bytes of b as a little-endian integer.
+func u32(b []byte) uint32 {
+ return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
+}
+
+const chunkHeaderSize = 8
+
+// FourCC is a four character code.
+type FourCC [4]byte
+
+// LIST is the "LIST" FourCC.
+var LIST = FourCC{'L', 'I', 'S', 'T'}
+
+// NewReader returns the RIFF stream's form type, such as "AVI " or "WAVE", and
+// its chunks as a *Reader.
+func NewReader(r io.Reader) (formType FourCC, data *Reader, err error) {
+ var buf [chunkHeaderSize]byte
+ if _, err := io.ReadFull(r, buf[:]); err != nil {
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ err = errMissingRIFFChunkHeader
+ }
+ return FourCC{}, nil, err
+ }
+ if buf[0] != 'R' || buf[1] != 'I' || buf[2] != 'F' || buf[3] != 'F' {
+ return FourCC{}, nil, errMissingRIFFChunkHeader
+ }
+ return NewListReader(u32(buf[4:]), r)
+}
+
+// NewListReader returns a LIST chunk's list type, such as "movi" or "wavl",
+// and its chunks as a *Reader.
+func NewListReader(chunkLen uint32, chunkData io.Reader) (listType FourCC, data *Reader, err error) {
+ if chunkLen < 4 {
+ return FourCC{}, nil, errShortChunkData
+ }
+ z := &Reader{r: chunkData}
+ if _, err := io.ReadFull(chunkData, z.buf[:4]); err != nil {
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ err = errShortChunkData
+ }
+ return FourCC{}, nil, err
+ }
+ z.totalLen = chunkLen - 4
+ return FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]}, z, nil
+}
+
+// Reader reads chunks from an underlying io.Reader.
+type Reader struct {
+ r io.Reader
+ err error
+
+ totalLen uint32
+ chunkLen uint32
+
+ chunkReader *chunkReader
+ buf [chunkHeaderSize]byte
+ padded bool
+}
+
+// Next returns the next chunk's ID, length and data. It returns io.EOF if there
+// are no more chunks. The io.Reader returned becomes stale after the next Next
+// call, and should no longer be used.
+//
+// It is valid to call Next even if all of the previous chunk's data has not
+// been read.
+func (z *Reader) Next() (chunkID FourCC, chunkLen uint32, chunkData io.Reader, err error) {
+ if z.err != nil {
+ return FourCC{}, 0, nil, z.err
+ }
+
+ // Drain the rest of the previous chunk.
+ if z.chunkLen != 0 {
+ _, z.err = io.Copy(ioutil.Discard, z.chunkReader)
+ if z.err != nil {
+ return FourCC{}, 0, nil, z.err
+ }
+ }
+ z.chunkReader = nil
+ if z.padded {
+ _, z.err = io.ReadFull(z.r, z.buf[:1])
+ if z.err != nil {
+ if z.err == io.EOF {
+ z.err = errMissingPaddingByte
+ }
+ return FourCC{}, 0, nil, z.err
+ }
+ z.totalLen--
+ }
+
+ // We are done if we have no more data.
+ if z.totalLen == 0 {
+ z.err = io.EOF
+ return FourCC{}, 0, nil, z.err
+ }
+
+ // Read the next chunk header.
+ if z.totalLen < chunkHeaderSize {
+ z.err = errShortChunkHeader
+ return FourCC{}, 0, nil, z.err
+ }
+ z.totalLen -= chunkHeaderSize
+ if _, err = io.ReadFull(z.r, z.buf[:chunkHeaderSize]); err != nil {
+ if z.err == io.EOF || z.err == io.ErrUnexpectedEOF {
+ z.err = errShortChunkHeader
+ }
+ return FourCC{}, 0, nil, z.err
+ }
+ chunkID = FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]}
+ chunkLen = u32(z.buf[4:])
+ z.chunkLen = chunkLen
+ z.padded = chunkLen&1 == 1
+ z.chunkReader = &chunkReader{z}
+ return chunkID, chunkLen, z.chunkReader, nil
+}
+
+type chunkReader struct {
+ z *Reader
+}
+
+func (c *chunkReader) Read(p []byte) (int, error) {
+ if c != c.z.chunkReader {
+ return 0, errStaleReader
+ }
+ z := c.z
+ if z.err != nil {
+ if z.err == io.EOF {
+ return 0, errStaleReader
+ }
+ return 0, z.err
+ }
+
+ n := int(z.chunkLen)
+ if n == 0 {
+ return 0, io.EOF
+ }
+ if n < 0 {
+ // Converting uint32 to int overflowed.
+ n = math.MaxInt32
+ }
+ if n > len(p) {
+ n = len(p)
+ }
+ n, err := z.r.Read(p[:n])
+ z.totalLen -= uint32(n)
+ z.chunkLen -= uint32(n)
+ if err != io.EOF {
+ z.err = err
+ }
+ return n, err
+}
diff --git a/webp/decode.go b/webp/decode.go
index 8cf874b..c688292 100644
--- a/webp/decode.go
+++ b/webp/decode.go
@@ -15,182 +15,171 @@
"image/color"
"io"
+ "code.google.com/p/go.image/riff"
"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
-}
+var errInvalidFormat = errors.New("webp: invalid format")
-const (
- formatVP8 = 1
- formatVP8L = 2
- formatVP8X = 3
+var (
+ fccALPH = riff.FourCC{'A', 'L', 'P', 'H'}
+ fccVP8 = riff.FourCC{'V', 'P', '8', ' '}
+ fccVP8L = riff.FourCC{'V', 'P', '8', 'L'}
+ fccVP8X = riff.FourCC{'V', 'P', '8', 'X'}
+ fccWEBP = riff.FourCC{'W', 'E', 'B', 'P'}
)
func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) {
- var b [20]byte
- if _, err := io.ReadFull(r, b[:]); err != nil {
+ formType, riffReader, err := riff.NewReader(r)
+ if err != nil {
return nil, image.Config{}, err
}
- format := 0
- switch string(b[8:16]) {
- case "WEBPVP8 ":
- 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 := 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 == 0 || dataLen >= 1<<31 {
- return nil, image.Config{}, errors.New("webp: invalid format")
- }
-
- 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
+ if formType != fccWEBP {
+ return nil, image.Config{}, errInvalidFormat
}
var (
- alpha []byte
- alphaStride int
+ alpha []byte
+ alphaStride int
+ wantAlpha bool
+ widthMinusOne uint32
+ heightMinusOne uint32
+ buf [10]byte
)
- if format == formatVP8X {
- if dataLen != 10 {
- return nil, image.Config{}, errors.New("webp: invalid format")
+ for {
+ chunkID, chunkLen, chunkData, err := riffReader.Next()
+ if err == io.EOF {
+ err = errInvalidFormat
}
- 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
}
- // 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")
- }
- }
+ switch chunkID {
+ case fccALPH:
+ if !wantAlpha {
+ return nil, image.Config{}, errInvalidFormat
+ }
+ wantAlpha = false
+ // Read the Pre-processing | Filter | Compression byte.
+ if _, err := io.ReadFull(chunkData, buf[:1]); err != nil {
+ if err == io.EOF {
+ err = errInvalidFormat
+ }
+ return nil, image.Config{}, err
+ }
+ filter := (buf[0] >> 2) & 0x03
+ if filter != 0 {
+ return nil, image.Config{}, errors.New(
+ "webp: VP8X Alpha filtering != 0 is not implemented")
+ }
+ compression := buf[0] & 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")
+ }
+ buf[0] = 0x2f // VP8L magic number.
+ buf[1] = uint8(widthMinusOne)
+ buf[2] = uint8(widthMinusOne>>8) | uint8(heightMinusOne<<6)
+ buf[3] = uint8(heightMinusOne >> 2)
+ buf[4] = uint8(heightMinusOne >> 10)
+ alphaImage, err := vp8l.Decode(io.MultiReader(
+ bytes.NewReader(buf[:5]),
+ chunkData,
+ ))
+ if err != nil {
+ return nil, image.Config{}, err
+ }
+ // 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
- d := vp8.NewDecoder()
- d.Init(r, int(dataLen))
- fh, err := d.DecodeFrameHeader()
- if err != nil {
- return nil, image.Config{}, err
+ case fccVP8:
+ if wantAlpha {
+ return nil, image.Config{}, errInvalidFormat
+ }
+ d := vp8.NewDecoder()
+ d.Init(chunkData, int(chunkLen))
+ fh, err := d.DecodeFrameHeader()
+ if err != nil {
+ return nil, 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
+
+ case fccVP8L:
+ if wantAlpha || alpha != nil {
+ return nil, image.Config{}, errInvalidFormat
+ }
+ if configOnly {
+ c, err := vp8l.DecodeConfig(chunkData)
+ return nil, c, err
+ }
+ m, err := vp8l.Decode(chunkData)
+ return m, image.Config{}, err
+
+ case fccVP8X:
+ if chunkLen != 10 {
+ return nil, image.Config{}, errInvalidFormat
+ }
+ if _, err := io.ReadFull(chunkData, buf[: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 buf[0] != alphaBit {
+ return nil, image.Config{}, errors.New("webp: non-Alpha VP8X is not implemented")
+ }
+ widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16
+ heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16
+ if configOnly {
+ return nil, image.Config{
+ ColorModel: nycbcra.ColorModel,
+ Width: int(widthMinusOne) + 1,
+ Height: int(heightMinusOne) + 1,
+ }, nil
+ }
+ wantAlpha = true
+ }
}
- 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.