diff options
Diffstat (limited to 'libgo/go/image/gif')
-rw-r--r-- | libgo/go/image/gif/reader.go | 159 | ||||
-rw-r--r-- | libgo/go/image/gif/reader_test.go | 49 | ||||
-rw-r--r-- | libgo/go/image/gif/writer.go | 127 | ||||
-rw-r--r-- | libgo/go/image/gif/writer_test.go | 280 |
4 files changed, 495 insertions, 120 deletions
diff --git a/libgo/go/image/gif/reader.go b/libgo/go/image/gif/reader.go index 5a863e204f3..6a133124ad5 100644 --- a/libgo/go/image/gif/reader.go +++ b/libgo/go/image/gif/reader.go @@ -32,15 +32,20 @@ type reader interface { // Masks etc. const ( // Fields. - fColorMapFollows = 1 << 7 - - // Image fields. - ifLocalColorTable = 1 << 7 - ifInterlace = 1 << 6 - ifPixelSizeMask = 7 + fColorTable = 1 << 7 + fInterlace = 1 << 6 + fColorTableBitsMask = 7 // Graphic control flags. gcTransparentColorSet = 1 << 0 + gcDisposalMethodMask = 7 << 2 +) + +// Disposal Methods. +const ( + DisposalNone = 0x01 + DisposalBackground = 0x02 + DisposalPrevious = 0x03 ) // Section indicators. @@ -66,14 +71,10 @@ type decoder struct { vers string width int height int - flags byte - headerFields byte - backgroundIndex byte loopCount int delayTime int - - // Unused from header. - aspect byte + backgroundIndex byte + disposalMethod byte // From image descriptor. imageFields byte @@ -83,13 +84,13 @@ type decoder struct { hasTransparentIndex bool // Computed. - pixelSize uint - globalColorMap color.Palette + globalColorTable color.Palette // Used when decoding. - delay []int - image []*image.Paletted - tmp [1024]byte // must be at least 768 so we can read color map + delay []int + disposal []byte + image []*image.Paletted + tmp [1024]byte // must be at least 768 so we can read color table } // blockReader parses the block structure of GIF image data, which @@ -122,7 +123,7 @@ func (b *blockReader) Read(p []byte) (int, error) { b.err = io.EOF return 0, b.err } - b.slice = b.tmp[0:blockLen] + b.slice = b.tmp[:blockLen] if _, b.err = io.ReadFull(b.r, b.slice); b.err != nil { return 0, b.err } @@ -149,12 +150,6 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { return nil } - if d.headerFields&fColorMapFollows != 0 { - if d.globalColorMap, err = d.readColorMap(); err != nil { - return err - } - } - for { c, err := d.r.ReadByte() if err != nil { @@ -171,19 +166,22 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { if err != nil { return err } - useLocalColorMap := d.imageFields&fColorMapFollows != 0 - if useLocalColorMap { - m.Palette, err = d.readColorMap() + useLocalColorTable := d.imageFields&fColorTable != 0 + if useLocalColorTable { + m.Palette, err = d.readColorTable(d.imageFields) if err != nil { return err } } else { - m.Palette = d.globalColorMap + if d.globalColorTable == nil { + return errors.New("gif: no color table") + } + m.Palette = d.globalColorTable } if d.hasTransparentIndex && int(d.transparentIndex) < len(m.Palette) { - if !useLocalColorMap { - // Clone the global color map. - m.Palette = append(color.Palette(nil), d.globalColorMap...) + if !useLocalColorTable { + // Clone the global color table. + m.Palette = append(color.Palette(nil), d.globalColorTable...) } m.Palette[d.transparentIndex] = color.RGBA{} } @@ -204,9 +202,18 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { } return errNotEnough } - // Both lzwr and br should be exhausted. Reading from them - // should yield (0, io.EOF). - if n, err := lzwr.Read(d.tmp[:1]); n != 0 || err != io.EOF { + // Both lzwr and br should be exhausted. Reading from them should + // yield (0, io.EOF). + // + // The spec (Appendix F - Compression), says that "An End of + // Information code... must be the last code output by the encoder + // for an image". In practice, though, giflib (a widely used C + // library) does not enforce this, so we also accept lzwr returning + // io.ErrUnexpectedEOF (meaning that the encoded stream hit io.EOF + // before the LZW decoder saw an explict end code), provided that + // the io.ReadFull call above successfully read len(m.Pix) bytes. + // See https://golang.org/issue/9856 for an example GIF. + if n, err := lzwr.Read(d.tmp[:1]); n != 0 || (err != io.EOF && err != io.ErrUnexpectedEOF) { if err != nil { return err } @@ -229,12 +236,13 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { } // Undo the interlacing if necessary. - if d.imageFields&ifInterlace != 0 { + if d.imageFields&fInterlace != 0 { uninterlace(m) } d.image = append(d.image, m) d.delay = append(d.delay, d.delayTime) + d.disposal = append(d.disposal, d.disposalMethod) // The GIF89a spec, Section 23 (Graphic Control Extension) says: // "The scope of this extension is the first graphic rendering block // to follow." We therefore reset the GCE fields to zero. @@ -254,44 +262,39 @@ func (d *decoder) decode(r io.Reader, configOnly bool) error { } func (d *decoder) readHeaderAndScreenDescriptor() error { - _, err := io.ReadFull(d.r, d.tmp[0:13]) + _, err := io.ReadFull(d.r, d.tmp[:13]) if err != nil { return err } - d.vers = string(d.tmp[0:6]) + d.vers = string(d.tmp[:6]) if d.vers != "GIF87a" && d.vers != "GIF89a" { return fmt.Errorf("gif: can't recognize format %s", d.vers) } d.width = int(d.tmp[6]) + int(d.tmp[7])<<8 d.height = int(d.tmp[8]) + int(d.tmp[9])<<8 - d.headerFields = d.tmp[10] - d.backgroundIndex = d.tmp[11] - d.aspect = d.tmp[12] - d.loopCount = -1 - d.pixelSize = uint(d.headerFields&7) + 1 + if fields := d.tmp[10]; fields&fColorTable != 0 { + d.backgroundIndex = d.tmp[11] + // readColorTable overwrites the contents of d.tmp, but that's OK. + if d.globalColorTable, err = d.readColorTable(fields); err != nil { + return err + } + } + // d.tmp[12] is the Pixel Aspect Ratio, which is ignored. return nil } -func (d *decoder) readColorMap() (color.Palette, error) { - if d.pixelSize > 8 { - return nil, fmt.Errorf("gif: can't handle %d bits per pixel", d.pixelSize) - } - numColors := 1 << d.pixelSize - if d.imageFields&ifLocalColorTable != 0 { - numColors = 1 << ((d.imageFields & ifPixelSizeMask) + 1) - } - numValues := 3 * numColors - _, err := io.ReadFull(d.r, d.tmp[0:numValues]) +func (d *decoder) readColorTable(fields byte) (color.Palette, error) { + n := 1 << (1 + uint(fields&fColorTableBitsMask)) + _, err := io.ReadFull(d.r, d.tmp[:3*n]) if err != nil { - return nil, fmt.Errorf("gif: short read on color map: %s", err) + return nil, fmt.Errorf("gif: short read on color table: %s", err) } - colorMap := make(color.Palette, numColors) - j := 0 - for i := range colorMap { - colorMap[i] = color.RGBA{d.tmp[j+0], d.tmp[j+1], d.tmp[j+2], 0xFF} + j, p := 0, make(color.Palette, n) + for i := range p { + p[i] = color.RGBA{d.tmp[j+0], d.tmp[j+1], d.tmp[j+2], 0xFF} j += 3 } - return colorMap, nil + return p, nil } func (d *decoder) readExtension() error { @@ -318,7 +321,7 @@ func (d *decoder) readExtension() error { return fmt.Errorf("gif: unknown extension 0x%.2x", extension) } if size > 0 { - if _, err := io.ReadFull(d.r, d.tmp[0:size]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[:size]); err != nil { return err } } @@ -343,12 +346,13 @@ func (d *decoder) readExtension() error { } func (d *decoder) readGraphicControl() error { - if _, err := io.ReadFull(d.r, d.tmp[0:6]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[:6]); err != nil { return fmt.Errorf("gif: can't read graphic control: %s", err) } - d.flags = d.tmp[1] + flags := d.tmp[1] + d.disposalMethod = (flags & gcDisposalMethodMask) >> 2 d.delayTime = int(d.tmp[2]) | int(d.tmp[3])<<8 - if d.flags&gcTransparentColorSet != 0 { + if flags&gcTransparentColorSet != 0 { d.transparentIndex = d.tmp[4] d.hasTransparentIndex = true } @@ -356,7 +360,7 @@ func (d *decoder) readGraphicControl() error { } func (d *decoder) newImageFromDescriptor() (*image.Paletted, error) { - if _, err := io.ReadFull(d.r, d.tmp[0:9]); err != nil { + if _, err := io.ReadFull(d.r, d.tmp[:9]); err != nil { return nil, fmt.Errorf("gif: can't read image descriptor: %s", err) } left := int(d.tmp[0]) + int(d.tmp[1])<<8 @@ -380,7 +384,7 @@ func (d *decoder) readBlock() (int, error) { if n == 0 || err != nil { return 0, err } - return io.ReadFull(d.r, d.tmp[0:n]) + return io.ReadFull(d.r, d.tmp[:n]) } // interlaceScan defines the ordering for a pass of the interlace algorithm. @@ -429,6 +433,24 @@ type GIF struct { Image []*image.Paletted // The successive images. Delay []int // The successive delay times, one per frame, in 100ths of a second. LoopCount int // The loop count. + // Disposal is the successive disposal methods, one per frame. For + // backwards compatibility, a nil Disposal is valid to pass to EncodeAll, + // and implies that each frame's disposal method is 0 (no disposal + // specified). + Disposal []byte + // Config is the global color table (palette), width and height. A nil or + // empty-color.Palette Config.ColorModel means that each frame has its own + // color table and there is no global color table. Each frame's bounds must + // be within the rectangle defined by the two points (0, 0) and + // (Config.Width, Config.Height). + // + // For backwards compatibility, a zero-valued Config is valid to pass to + // EncodeAll, and implies that the overall GIF's width and height equals + // the first frame's bounds' Rectangle.Max point. + Config image.Config + // BackgroundIndex is the background index in the global color table, for + // use with the DisposalBackground disposal method. + BackgroundIndex byte } // DecodeAll reads a GIF image from r and returns the sequential frames @@ -442,6 +464,13 @@ func DecodeAll(r io.Reader) (*GIF, error) { Image: d.image, LoopCount: d.loopCount, Delay: d.delay, + Disposal: d.disposal, + Config: image.Config{ + ColorModel: d.globalColorTable, + Width: d.width, + Height: d.height, + }, + BackgroundIndex: d.backgroundIndex, } return gif, nil } @@ -454,7 +483,7 @@ func DecodeConfig(r io.Reader) (image.Config, error) { return image.Config{}, err } return image.Config{ - ColorModel: d.globalColorMap, + ColorModel: d.globalColorTable, Width: d.width, Height: d.height, }, nil diff --git a/libgo/go/image/gif/reader_test.go b/libgo/go/image/gif/reader_test.go index 7b6f504367c..c294195b6f7 100644 --- a/libgo/go/image/gif/reader_test.go +++ b/libgo/go/image/gif/reader_test.go @@ -17,8 +17,8 @@ import ( const ( headerStr = "GIF89a" + "\x02\x00\x01\x00" + // width=2, height=1 - "\x80\x00\x00" // headerFields=(a color map of 2 pixels), backgroundIndex, aspect - paletteStr = "\x10\x20\x30\x40\x50\x60" // the color map, also known as a palette + "\x80\x00\x00" // headerFields=(a color table of 2 pixels), backgroundIndex, aspect + paletteStr = "\x10\x20\x30\x40\x50\x60" // the color table, also known as a palette trailerStr = "\x3b" ) @@ -141,7 +141,7 @@ var testGIF = []byte{ 'G', 'I', 'F', '8', '9', 'a', 1, 0, 1, 0, // w=1, h=1 (6) 128, 0, 0, // headerFields, bg, aspect (10) - 0, 0, 0, 1, 1, 1, // color map and graphics control (13) + 0, 0, 0, 1, 1, 1, // color table and graphics control (13) 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, // (19) // frame 1 (0,0 - 1,1) 0x2c, @@ -200,22 +200,26 @@ func TestNoPalette(t *testing.T) { b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02") // Encode the pixels: neither is in range, because there is no palette. - pix := []byte{0, 128} + pix := []byte{0, 3} enc := &bytes.Buffer{} w := lzw.NewWriter(enc, lzw.LSB, 2) - w.Write(pix) - w.Close() + if _, err := w.Write(pix); err != nil { + t.Fatalf("Write: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Close: %v", err) + } b.WriteByte(byte(len(enc.Bytes()))) b.Write(enc.Bytes()) b.WriteByte(0x00) // An empty block signifies the end of the image data. b.WriteString(trailerStr) - try(t, b.Bytes(), "gif: invalid pixel value") + try(t, b.Bytes(), "gif: no color table") } func TestPixelOutsidePaletteRange(t *testing.T) { - for _, pval := range []byte{0, 1, 2, 3, 255} { + for _, pval := range []byte{0, 1, 2, 3} { b := &bytes.Buffer{} // Manufacture a GIF with a 2 color palette. @@ -229,8 +233,12 @@ func TestPixelOutsidePaletteRange(t *testing.T) { pix := []byte{pval, pval} enc := &bytes.Buffer{} w := lzw.NewWriter(enc, lzw.LSB, 2) - w.Write(pix) - w.Close() + if _, err := w.Write(pix); err != nil { + t.Fatalf("Write: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Close: %v", err) + } b.WriteByte(byte(len(enc.Bytes()))) b.Write(enc.Bytes()) b.WriteByte(0x00) // An empty block signifies the end of the image data. @@ -245,3 +253,24 @@ func TestPixelOutsidePaletteRange(t *testing.T) { try(t, b.Bytes(), want) } } + +func TestLoopCount(t *testing.T) { + data := []byte("GIF89a000\x00000,0\x00\x00\x00\n\x00" + + "\n\x00\x80000000\x02\b\xf01u\xb9\xfdal\x05\x00;") + img, err := DecodeAll(bytes.NewReader(data)) + if err != nil { + t.Fatal("DecodeAll:", err) + } + w := new(bytes.Buffer) + err = EncodeAll(w, img) + if err != nil { + t.Fatal("EncodeAll:", err) + } + img1, err := DecodeAll(w) + if err != nil { + t.Fatal("DecodeAll:", err) + } + if img.LoopCount != img1.LoopCount { + t.Errorf("loop count mismatch: %d vs %d", img.LoopCount, img1.LoopCount) + } +} diff --git a/libgo/go/image/gif/writer.go b/libgo/go/image/gif/writer.go index 49abde704c8..dd317901d48 100644 --- a/libgo/go/image/gif/writer.go +++ b/libgo/go/image/gif/writer.go @@ -6,6 +6,7 @@ package gif import ( "bufio" + "bytes" "compress/lzw" "errors" "image" @@ -52,9 +53,13 @@ type encoder struct { w writer err error // g is a reference to the data that is being encoded. - g *GIF - // buf is a scratch buffer. It must be at least 768 so we can write the color map. - buf [1024]byte + g GIF + // globalCT is the size in bytes of the global color table. + globalCT int + // buf is a scratch buffer. It must be at least 256 for the blockWriter. + buf [256]byte + globalColorTable [3 * 256]byte + localColorTable [3 * 256]byte } // blockWriter writes the block structure of GIF image data, which @@ -116,18 +121,27 @@ func (e *encoder) writeHeader() { return } - pm := e.g.Image[0] // Logical screen width and height. - writeUint16(e.buf[0:2], uint16(pm.Bounds().Dx())) - writeUint16(e.buf[2:4], uint16(pm.Bounds().Dy())) + writeUint16(e.buf[0:2], uint16(e.g.Config.Width)) + writeUint16(e.buf[2:4], uint16(e.g.Config.Height)) e.write(e.buf[:4]) - // All frames have a local color table, so a global color table - // is not needed. - e.buf[0] = 0x00 - e.buf[1] = 0x00 // Background Color Index. - e.buf[2] = 0x00 // Pixel Aspect Ratio. - e.write(e.buf[:3]) + if p, ok := e.g.Config.ColorModel.(color.Palette); ok && len(p) > 0 { + paddedSize := log2(len(p)) // Size of Global Color Table: 2^(1+n). + e.buf[0] = fColorTable | uint8(paddedSize) + e.buf[1] = e.g.BackgroundIndex + e.buf[2] = 0x00 // Pixel Aspect Ratio. + e.write(e.buf[:3]) + e.globalCT = encodeColorTable(e.globalColorTable[:], p, paddedSize) + e.write(e.globalColorTable[:e.globalCT]) + } else { + // All frames have a local color table, so a global color table + // is not needed. + e.buf[0] = 0x00 + e.buf[1] = 0x00 // Background Color Index. + e.buf[2] = 0x00 // Pixel Aspect Ratio. + e.write(e.buf[:3]) + } // Add animation info if necessary. if len(e.g.Image) > 1 { @@ -147,28 +161,25 @@ func (e *encoder) writeHeader() { } } -func (e *encoder) writeColorTable(p color.Palette, size int) { - if e.err != nil { - return - } - - for i := 0; i < log2Lookup[size]; i++ { +func encodeColorTable(dst []byte, p color.Palette, size int) int { + n := log2Lookup[size] + for i := 0; i < n; i++ { if i < len(p) { r, g, b, _ := p[i].RGBA() - e.buf[3*i+0] = uint8(r >> 8) - e.buf[3*i+1] = uint8(g >> 8) - e.buf[3*i+2] = uint8(b >> 8) + dst[3*i+0] = uint8(r >> 8) + dst[3*i+1] = uint8(g >> 8) + dst[3*i+2] = uint8(b >> 8) } else { // Pad with black. - e.buf[3*i+0] = 0x00 - e.buf[3*i+1] = 0x00 - e.buf[3*i+2] = 0x00 + dst[3*i+0] = 0x00 + dst[3*i+1] = 0x00 + dst[3*i+2] = 0x00 } } - e.write(e.buf[:3*log2Lookup[size]]) + return 3 * n } -func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { +func (e *encoder) writeImageBlock(pm *image.Paletted, delay int, disposal byte) { if e.err != nil { return } @@ -179,10 +190,14 @@ func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { } b := pm.Bounds() - if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 || b.Min.X < 0 || b.Min.X >= 1<<16 || b.Min.Y < 0 || b.Min.Y >= 1<<16 { + if b.Min.X < 0 || b.Max.X >= 1<<16 || b.Min.Y < 0 || b.Max.Y >= 1<<16 { e.err = errors.New("gif: image block is too large to encode") return } + if !b.In(image.Rectangle{Max: image.Point{e.g.Config.Width, e.g.Config.Height}}) { + e.err = errors.New("gif: image block is out of bounds") + return + } transparentIndex := -1 for i, c := range pm.Palette { @@ -192,14 +207,14 @@ func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { } } - if delay > 0 || transparentIndex != -1 { + if delay > 0 || disposal != 0 || transparentIndex != -1 { e.buf[0] = sExtension // Extension Introducer. e.buf[1] = gcLabel // Graphic Control Label. e.buf[2] = gcBlockSize // Block Size. if transparentIndex != -1 { - e.buf[3] = 0x01 + e.buf[3] = 0x01 | disposal<<2 } else { - e.buf[3] = 0x00 + e.buf[3] = 0x00 | disposal<<2 } writeUint16(e.buf[4:6], uint16(delay)) // Delay Time (1/100ths of a second) @@ -220,11 +235,15 @@ func (e *encoder) writeImageBlock(pm *image.Paletted, delay int) { e.write(e.buf[:9]) paddedSize := log2(len(pm.Palette)) // Size of Local Color Table: 2^(1+n). - // Interlacing is not supported. - e.writeByte(0x80 | uint8(paddedSize)) - - // Local Color Table. - e.writeColorTable(pm.Palette, paddedSize) + ct := encodeColorTable(e.localColorTable[:], pm.Palette, paddedSize) + if ct != e.globalCT || !bytes.Equal(e.globalColorTable[:ct], e.localColorTable[:ct]) { + // Use a local color table. + e.writeByte(fColorTable | uint8(paddedSize)) + e.write(e.localColorTable[:ct]) + } else { + // Use the global color table. + e.writeByte(0) + } litWidth := paddedSize + 1 if litWidth < 2 { @@ -281,7 +300,23 @@ func EncodeAll(w io.Writer, g *GIF) error { g.LoopCount = 0 } - e := encoder{g: g} + e := encoder{g: *g} + // The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added + // in Go 1.5. Valid Go 1.4 code, such as when the Disposal field is omitted + // in a GIF struct literal, should still produce valid GIFs. + if e.g.Disposal != nil && len(e.g.Image) != len(e.g.Disposal) { + return errors.New("gif: mismatched image and disposal lengths") + } + if e.g.Config == (image.Config{}) { + p := g.Image[0].Bounds().Max + e.g.Config.Width = p.X + e.g.Config.Height = p.Y + } else if e.g.Config.ColorModel != nil { + if _, ok := e.g.Config.ColorModel.(color.Palette); !ok { + return errors.New("gif: GIF color model must be a color.Palette") + } + } + if ww, ok := w.(writer); ok { e.w = ww } else { @@ -290,7 +325,11 @@ func EncodeAll(w io.Writer, g *GIF) error { e.writeHeader() for i, pm := range g.Image { - e.writeImageBlock(pm, g.Delay[i]) + disposal := uint8(0) + if g.Disposal != nil { + disposal = g.Disposal[i] + } + e.writeImageBlock(pm, g.Delay[i], disposal) } e.writeByte(sTrailer) e.flush() @@ -326,8 +365,22 @@ func Encode(w io.Writer, m image.Image, o *Options) error { opts.Drawer.Draw(pm, b, m, image.ZP) } + // When calling Encode instead of EncodeAll, the single-frame image is + // translated such that its top-left corner is (0, 0), so that the single + // frame completely fills the overall GIF's bounds. + if pm.Rect.Min != (image.Point{}) { + dup := *pm + dup.Rect = dup.Rect.Sub(dup.Rect.Min) + pm = &dup + } + return EncodeAll(w, &GIF{ Image: []*image.Paletted{pm}, Delay: []int{0}, + Config: image.Config{ + ColorModel: pm.Palette, + Width: b.Dx(), + Height: b.Dy(), + }, }) } diff --git a/libgo/go/image/gif/writer_test.go b/libgo/go/image/gif/writer_test.go index 93306ffdb34..db61a5c3c2e 100644 --- a/libgo/go/image/gif/writer_test.go +++ b/libgo/go/image/gif/writer_test.go @@ -8,10 +8,12 @@ import ( "bytes" "image" "image/color" + "image/color/palette" _ "image/png" "io/ioutil" "math/rand" "os" + "reflect" "testing" ) @@ -125,55 +127,317 @@ func TestSubImage(t *testing.T) { } } +// palettesEqual reports whether two color.Palette values are equal, ignoring +// any trailing opaque-black palette entries. +func palettesEqual(p, q color.Palette) bool { + n := len(p) + if n > len(q) { + n = len(q) + } + for i := 0; i < n; i++ { + if p[i] != q[i] { + return false + } + } + for i := n; i < len(p); i++ { + r, g, b, a := p[i].RGBA() + if r != 0 || g != 0 || b != 0 || a != 0xffff { + return false + } + } + for i := n; i < len(q); i++ { + r, g, b, a := q[i].RGBA() + if r != 0 || g != 0 || b != 0 || a != 0xffff { + return false + } + } + return true +} + var frames = []string{ "../testdata/video-001.gif", "../testdata/video-005.gray.gif", } -func TestEncodeAll(t *testing.T) { +func testEncodeAll(t *testing.T, go1Dot5Fields bool, useGlobalColorModel bool) { + const width, height = 150, 103 + g0 := &GIF{ Image: make([]*image.Paletted, len(frames)), Delay: make([]int, len(frames)), LoopCount: 5, } for i, f := range frames { - m, err := readGIF(f) + g, err := readGIF(f) if err != nil { t.Fatal(f, err) } - g0.Image[i] = m.Image[0] + m := g.Image[0] + if m.Bounds().Dx() != width || m.Bounds().Dy() != height { + t.Fatalf("frame %d had unexpected bounds: got %v, want width/height = %d/%d", + i, m.Bounds(), width, height) + } + g0.Image[i] = m } + // The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added + // in Go 1.5. Valid Go 1.4 or earlier code should still produce valid GIFs. + // + // On the following line, color.Model is an interface type, and + // color.Palette is a concrete (slice) type. + globalColorModel, backgroundIndex := color.Model(color.Palette(nil)), uint8(0) + if useGlobalColorModel { + globalColorModel, backgroundIndex = color.Palette(palette.WebSafe), uint8(1) + } + if go1Dot5Fields { + g0.Disposal = make([]byte, len(g0.Image)) + for i := range g0.Disposal { + g0.Disposal[i] = DisposalNone + } + g0.Config = image.Config{ + ColorModel: globalColorModel, + Width: width, + Height: height, + } + g0.BackgroundIndex = backgroundIndex + } + var buf bytes.Buffer if err := EncodeAll(&buf, g0); err != nil { t.Fatal("EncodeAll:", err) } - g1, err := DecodeAll(&buf) + encoded := buf.Bytes() + config, err := DecodeConfig(bytes.NewReader(encoded)) + if err != nil { + t.Fatal("DecodeConfig:", err) + } + g1, err := DecodeAll(bytes.NewReader(encoded)) if err != nil { t.Fatal("DecodeAll:", err) } + + if !reflect.DeepEqual(config, g1.Config) { + t.Errorf("DecodeConfig inconsistent with DecodeAll") + } + if !palettesEqual(g1.Config.ColorModel.(color.Palette), globalColorModel.(color.Palette)) { + t.Errorf("unexpected global color model") + } + if w, h := g1.Config.Width, g1.Config.Height; w != width || h != height { + t.Errorf("got config width * height = %d * %d, want %d * %d", w, h, width, height) + } + if g0.LoopCount != g1.LoopCount { t.Errorf("loop counts differ: %d and %d", g0.LoopCount, g1.LoopCount) } + if backgroundIndex != g1.BackgroundIndex { + t.Errorf("background indexes differ: %d and %d", backgroundIndex, g1.BackgroundIndex) + } + if len(g0.Image) != len(g1.Image) { + t.Fatalf("image lengths differ: %d and %d", len(g0.Image), len(g1.Image)) + } + if len(g1.Image) != len(g1.Delay) { + t.Fatalf("image and delay lengths differ: %d and %d", len(g1.Image), len(g1.Delay)) + } + if len(g1.Image) != len(g1.Disposal) { + t.Fatalf("image and disposal lengths differ: %d and %d", len(g1.Image), len(g1.Disposal)) + } + for i := range g0.Image { m0, m1 := g0.Image[i], g1.Image[i] if m0.Bounds() != m1.Bounds() { - t.Errorf("%s, bounds differ: %v and %v", frames[i], m0.Bounds(), m1.Bounds()) + t.Errorf("frame %d: bounds differ: %v and %v", i, m0.Bounds(), m1.Bounds()) } d0, d1 := g0.Delay[i], g1.Delay[i] if d0 != d1 { - t.Errorf("%s: delay values differ: %d and %d", frames[i], d0, d1) + t.Errorf("frame %d: delay values differ: %d and %d", i, d0, d1) + } + p0, p1 := uint8(0), g1.Disposal[i] + if go1Dot5Fields { + p0 = DisposalNone + } + if p0 != p1 { + t.Errorf("frame %d: disposal values differ: %d and %d", i, p0, p1) } } +} - g1.Delay = make([]int, 1) - if err := EncodeAll(ioutil.Discard, g1); err == nil { +func TestEncodeAllGo1Dot4(t *testing.T) { testEncodeAll(t, false, false) } +func TestEncodeAllGo1Dot5(t *testing.T) { testEncodeAll(t, true, false) } +func TestEncodeAllGo1Dot5GlobalColorModel(t *testing.T) { testEncodeAll(t, true, true) } + +func TestEncodeMismatchDelay(t *testing.T) { + images := make([]*image.Paletted, 2) + for i := range images { + images[i] = image.NewPaletted(image.Rect(0, 0, 5, 5), palette.Plan9) + } + + g0 := &GIF{ + Image: images, + Delay: make([]int, 1), + } + if err := EncodeAll(ioutil.Discard, g0); err == nil { t.Error("expected error from mismatched delay and image slice lengths") } + + g1 := &GIF{ + Image: images, + Delay: make([]int, len(images)), + Disposal: make([]byte, 1), + } + for i := range g1.Disposal { + g1.Disposal[i] = DisposalNone + } + if err := EncodeAll(ioutil.Discard, g1); err == nil { + t.Error("expected error from mismatched disposal and image slice lengths") + } +} + +func TestEncodeZeroGIF(t *testing.T) { if err := EncodeAll(ioutil.Discard, &GIF{}); err == nil { t.Error("expected error from providing empty gif") } } +func TestEncodeAllFramesOutOfBounds(t *testing.T) { + images := []*image.Paletted{ + image.NewPaletted(image.Rect(0, 0, 5, 5), palette.Plan9), + image.NewPaletted(image.Rect(2, 2, 8, 8), palette.Plan9), + image.NewPaletted(image.Rect(3, 3, 4, 4), palette.Plan9), + } + for _, upperBound := range []int{6, 10} { + g := &GIF{ + Image: images, + Delay: make([]int, len(images)), + Disposal: make([]byte, len(images)), + Config: image.Config{ + Width: upperBound, + Height: upperBound, + }, + } + err := EncodeAll(ioutil.Discard, g) + if upperBound >= 8 { + if err != nil { + t.Errorf("upperBound=%d: %v", upperBound, err) + } + } else { + if err == nil { + t.Errorf("upperBound=%d: got nil error, want non-nil", upperBound) + } + } + } +} + +func TestEncodeNonZeroMinPoint(t *testing.T) { + points := []image.Point{ + image.Point{-8, -9}, + image.Point{-4, -4}, + image.Point{-3, +3}, + image.Point{+0, +0}, + image.Point{+2, +2}, + } + for _, p := range points { + src := image.NewPaletted(image.Rectangle{Min: p, Max: p.Add(image.Point{6, 6})}, palette.Plan9) + var buf bytes.Buffer + if err := Encode(&buf, src, nil); err != nil { + t.Errorf("p=%v: Encode: %v", p, err) + continue + } + m, err := Decode(&buf) + if err != nil { + t.Errorf("p=%v: Decode: %v", p, err) + continue + } + if got, want := m.Bounds(), image.Rect(0, 0, 6, 6); got != want { + t.Errorf("p=%v: got %v, want %v", p, got, want) + } + } +} + +func TestEncodeImplicitConfigSize(t *testing.T) { + // For backwards compatibility for Go 1.4 and earlier code, the Config + // field is optional, and if zero, the width and height is implied by the + // first (and in this case only) frame's width and height. + // + // A Config only specifies a width and height (two integers) while an + // image.Image's Bounds method returns an image.Rectangle (four integers). + // For a gif.GIF, the overall bounds' top-left point is always implicitly + // (0, 0), and any frame whose bounds have a negative X or Y will be + // outside those overall bounds, so encoding should fail. + for _, lowerBound := range []int{-1, 0, 1} { + images := []*image.Paletted{ + image.NewPaletted(image.Rect(lowerBound, lowerBound, 4, 4), palette.Plan9), + } + g := &GIF{ + Image: images, + Delay: make([]int, len(images)), + } + err := EncodeAll(ioutil.Discard, g) + if lowerBound >= 0 { + if err != nil { + t.Errorf("lowerBound=%d: %v", lowerBound, err) + } + } else { + if err == nil { + t.Errorf("lowerBound=%d: got nil error, want non-nil", lowerBound) + } + } + } +} + +func TestEncodePalettes(t *testing.T) { + const w, h = 5, 5 + pals := []color.Palette{{ + color.RGBA{0x00, 0x00, 0x00, 0xff}, + color.RGBA{0x01, 0x00, 0x00, 0xff}, + color.RGBA{0x02, 0x00, 0x00, 0xff}, + }, { + color.RGBA{0x00, 0x00, 0x00, 0xff}, + color.RGBA{0x00, 0x01, 0x00, 0xff}, + }, { + color.RGBA{0x00, 0x00, 0x03, 0xff}, + color.RGBA{0x00, 0x00, 0x02, 0xff}, + color.RGBA{0x00, 0x00, 0x01, 0xff}, + color.RGBA{0x00, 0x00, 0x00, 0xff}, + }, { + color.RGBA{0x10, 0x07, 0xf0, 0xff}, + color.RGBA{0x20, 0x07, 0xf0, 0xff}, + color.RGBA{0x30, 0x07, 0xf0, 0xff}, + color.RGBA{0x40, 0x07, 0xf0, 0xff}, + color.RGBA{0x50, 0x07, 0xf0, 0xff}, + }} + g0 := &GIF{ + Image: []*image.Paletted{ + image.NewPaletted(image.Rect(0, 0, w, h), pals[0]), + image.NewPaletted(image.Rect(0, 0, w, h), pals[1]), + image.NewPaletted(image.Rect(0, 0, w, h), pals[2]), + image.NewPaletted(image.Rect(0, 0, w, h), pals[3]), + }, + Delay: make([]int, len(pals)), + Disposal: make([]byte, len(pals)), + Config: image.Config{ + ColorModel: pals[2], + Width: w, + Height: h, + }, + } + + var buf bytes.Buffer + if err := EncodeAll(&buf, g0); err != nil { + t.Fatalf("EncodeAll: %v", err) + } + g1, err := DecodeAll(&buf) + if err != nil { + t.Fatalf("DecodeAll: %v", err) + } + if len(g0.Image) != len(g1.Image) { + t.Fatalf("image lengths differ: %d and %d", len(g0.Image), len(g1.Image)) + } + for i, m := range g1.Image { + if got, want := m.Palette, pals[i]; !palettesEqual(got, want) { + t.Errorf("frame %d:\ngot %v\nwant %v", i, got, want) + } + } +} + func BenchmarkEncode(b *testing.B) { b.StopTimer() |